From 2911d2ce4cc3b3fc1249400c6bc42ac5ddc059e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 22 Apr 2020 23:58:31 +0200 Subject: [PATCH 001/199] Ignore the root layout on error pages --- lib/phoenix/endpoint/render_errors.ex | 1 + test/phoenix/endpoint/render_errors_test.exs | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/phoenix/endpoint/render_errors.ex b/lib/phoenix/endpoint/render_errors.ex index 4dbcd09779..41e3f10e1e 100644 --- a/lib/phoenix/endpoint/render_errors.ex +++ b/lib/phoenix/endpoint/render_errors.ex @@ -103,6 +103,7 @@ defmodule Phoenix.Endpoint.RenderErrors do |> maybe_fetch_query_params() |> maybe_fetch_format(opts) |> Plug.Conn.put_status(status) + |> Controller.put_root_layout(opts[:root_layout] || false) |> Controller.put_layout(opts[:layout] || false) |> Controller.put_view(view) diff --git a/test/phoenix/endpoint/render_errors_test.exs b/test/phoenix/endpoint/render_errors_test.exs index c96c0167aa..168ff630ca 100644 --- a/test/phoenix/endpoint/render_errors_test.exs +++ b/test/phoenix/endpoint/render_errors_test.exs @@ -7,8 +7,8 @@ defmodule Phoenix.Endpoint.RenderErrorsTest do import ExUnit.CaptureLog view = __MODULE__ - def render("app.html", %{view_template: view_template} = assigns) do - "Layout: " <> render(view_template, assigns) + def render("app.html", assigns) do + "Layout: " <> assigns.inner_content end def render("404.html", %{kind: kind, reason: _reason, stack: _stack, status: 404, conn: conn}) do @@ -56,6 +56,12 @@ defmodule Phoenix.Endpoint.RenderErrorsTest do _ -> __STACKTRACE__ end + # Those are always ignored and must be explicitly opted-in. + conn = + conn + |> Phoenix.Controller.put_layout({Unknown, "layout"}) + |> Phoenix.Controller.put_root_layout({Unknown, "root"}) + reason = ArgumentError.exception("oops") raise Plug.Conn.WrapperError, conn: conn, kind: :error, stack: stack, reason: reason end @@ -252,6 +258,14 @@ defmodule Phoenix.Endpoint.RenderErrorsTest do assert body == "Layout: Got 500 from throw with GET" end + test "exception page with root layout" do + body = assert_render(500, conn(:get, "/"), [root_layout: {__MODULE__, :app}], fn -> + throw :hello + end) + + assert body == "Layout: Got 500 from throw with GET" + end + test "exception page is shown even with invalid format" do conn = conn(:get, "/") |> put_req_header("accept", "unknown/unknown") body = assert_render(500, conn, [], fn -> throw :hello end) From 955458939217b495685558314f218cc0fbe5cd19 Mon Sep 17 00:00:00 2001 From: Chris McCord Date: Thu, 23 Apr 2020 00:15:14 -0400 Subject: [PATCH 002/199] Release 1.5.1 --- CHANGELOG.md | 5 +++++ assets/package.json | 2 +- guides/introduction/installation.md | 2 +- installer/README.md | 2 +- installer/mix.exs | 2 +- mix.exs | 2 +- package.json | 2 +- 7 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2058e421c..1d1ec92c80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ Then once you start an application, you will get a warning about the `:pubsub` k Phoenix built-in guides have been restructured and revamped, providing a better navigation structure and more content. +## 1.5.1 (2020-04-23) + +### Bug Fixes + * [Endpoint] Ignore the root layout on error pages unless explicitly set + ## 1.5.0 (2020-04-22) ### Enhancements diff --git a/assets/package.json b/assets/package.json index e4344c0b71..81994acded 100644 --- a/assets/package.json +++ b/assets/package.json @@ -1,6 +1,6 @@ { "name": "phoenix", - "version": "1.5.0", + "version": "1.5.1", "description": "The official JavaScript client for the Phoenix web framework.", "license": "MIT", "main": "./priv/static/phoenix.js", diff --git a/guides/introduction/installation.md b/guides/introduction/installation.md index 43fb1e683c..33ea90ff3c 100644 --- a/guides/introduction/installation.md +++ b/guides/introduction/installation.md @@ -41,7 +41,7 @@ Elixir 1.6.3 Once we have Elixir and Erlang, we are ready to install the Phoenix application generator: ```console -$ mix archive.install hex phx_new 1.5.0 +$ mix archive.install hex phx_new 1.5.1 ``` We will use this generator to generate new applications in the next guide, called [Up and Running](up_and_running.html). diff --git a/installer/README.md b/installer/README.md index f155fda506..e641002297 100644 --- a/installer/README.md +++ b/installer/README.md @@ -4,7 +4,7 @@ Provides `phx.new` installer as an archive. To install from hex, run: - $ mix archive.install hex phx_new 1.5.0 + $ mix archive.install hex phx_new 1.5.1 To build and install it locally, ensure any previous archive versions are removed: diff --git a/installer/mix.exs b/installer/mix.exs index e42ce54f48..2d5a394816 100644 --- a/installer/mix.exs +++ b/installer/mix.exs @@ -1,7 +1,7 @@ defmodule Phx.New.MixProject do use Mix.Project - @version "1.5.0" + @version "1.5.1" @github_path "phoenixframework/phoenix" @url "https://github.com/#{@github_path}" diff --git a/mix.exs b/mix.exs index d81a689876..ea7cf38839 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Phoenix.MixProject do use Mix.Project - @version "1.5.0" + @version "1.5.1" # If the elixir requirement is updated, we need to make the installer # use at least the minimum requirement used here. Although often the diff --git a/package.json b/package.json index 07c0b317a7..5be14ba5f0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "phoenix", - "version": "1.5.0", + "version": "1.5.1", "description": "The official JavaScript client for the Phoenix web framework.", "license": "MIT", "main": "./priv/static/phoenix.js", From b0bb0ae236b836a00cbf313de653478d6fd1f0a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 23 Apr 2020 16:01:01 +0200 Subject: [PATCH 003/199] Fix guide link --- lib/phoenix.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix.ex b/lib/phoenix.ex index ce74672a8c..47d958490f 100644 --- a/lib/phoenix.ex +++ b/lib/phoenix.ex @@ -26,7 +26,7 @@ defmodule Phoenix do * [Gettext](https://hexdocs.pm/gettext) - Internationalization and localization through [`gettext`](https://www.gnu.org/software/gettext/) - To get started, see our [overview guides](/overview.html). + To get started, see our [overview guides](overview.html). """ use Application From a5edbc50dc3b62454062ca47dfe8c7be6f520838 Mon Sep 17 00:00:00 2001 From: Michael Wu Date: Thu, 23 Apr 2020 08:55:45 -0500 Subject: [PATCH 004/199] Fix guides: plug code typo (#3784) --- guides/plug.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/plug.md b/guides/plug.md index 5eb572f06d..33c1905b0e 100644 --- a/guides/plug.md +++ b/guides/plug.md @@ -214,7 +214,7 @@ In particular, controller plugs provide a feature that allows us to execute plug defmodule HelloWeb.HelloController do use HelloWeb, :controller - plug HelloWeb.Plugs.Locale, "en" when action_name in [:index] + plug HelloWeb.Plugs.Locale, "en" when action in [:index] ``` And the plug will only be executed for the `index` action. From 32249cf237b77dcf3d7782d5cbbfd8c8110b7273 Mon Sep 17 00:00:00 2001 From: Stephen Bussey Date: Thu, 23 Apr 2020 09:54:09 -0400 Subject: [PATCH 005/199] Add real-time phoenix to community.md (#3786) --- guides/introduction/community.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/guides/introduction/community.md b/guides/introduction/community.md index ac64b98846..0757cc0a3e 100644 --- a/guides/introduction/community.md +++ b/guides/introduction/community.md @@ -22,6 +22,8 @@ There are a number of places to connect with community members at all experience * [Programming Phoenix 1.4 (by Bruce Tate and Phoenix core team members Chris McCord and José Valim - Dec 2019)](https://pragprog.com/book/phoenix14/programming-phoenix-1-4) + * [Real-Time Phoenix - Build Highly Scalable Systems with Channels (by Stephen Bussey - 2020)](https://pragprog.com/book/sbsockets/real-time-phoenix) + * [Phoenix Inside Out - Book Series (by Shankar Dhanasekaran - July 2017)](https://shankardevy.com/phoenix-book/) * [Functional Web Development with Elixir, OTP, and Phoenix Rethink the Modern Web App (by Lance Halvorsen - Oct 2017)](https://pragprog.com/book/lhelph/functional-web-development-with-elixir-otp-and-phoenix) From 4076800135298e118728807bf4d8e0c2337d51a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Gibowski?= Date: Fri, 24 Apr 2020 15:09:46 +0100 Subject: [PATCH 006/199] Fix grammar in view guide (#3789) --- guides/views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/views.md b/guides/views.md index 5c93a7663c..3ccd6fc617 100644 --- a/guides/views.md +++ b/guides/views.md @@ -50,7 +50,7 @@ end When we reload our home page, we should see our new title. Since templates are compiled inside the view, we could invoke the view function simply as `title()`, otherwise we would have to type `HelloWeb.LayoutView.title()`. -As you may recall, Elixir templates use Embedded Elixir, known as `EEx`. We use `<%= expression %>` to execute Elixir expressions. The result of the expression is interpolated into the template. You can use pretty much use any Elixir expression. For example, in order to have conditionals: +As you may recall, Elixir templates use Embedded Elixir, known as `EEx`. We use `<%= expression %>` to execute Elixir expressions. The result of the expression is interpolated into the template. You can use pretty much any Elixir expression. For example, in order to have conditionals: ```html <%= if some_condition? do %> From db873e7d25bf5d5d7af0582565db3abb8b2ff726 Mon Sep 17 00:00:00 2001 From: Aaron Renner Date: Fri, 24 Apr 2020 09:12:56 -0600 Subject: [PATCH 007/199] Update various guides to link to mix tasks (#3790) --- guides/contexts.md | 12 ++++++------ guides/routing.md | 4 ++-- guides/testing/testing_controllers.md | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/guides/contexts.md b/guides/contexts.md index 26310a4f57..5baf7d17f1 100644 --- a/guides/contexts.md +++ b/guides/contexts.md @@ -24,7 +24,7 @@ Let's use these ideas to build out our web application. Our goal is to build a u User accounts are often wide-reaching across a platform so it's important to think upfront about writing a well-defined interface. With that in mind, our goal is to build an accounts API that handles creating, updating, and deleting user accounts, as well as authenticating user credentials. We'll start off with basic features, but as we add authentication later, we'll see how starting with a solid foundation allows us to grow our application naturally as we add functionality. -Phoenix includes the `phx.gen.html`, `phx.gen.json`, `phx.gen.live`, and `phx.gen.context` generators that apply the ideas of isolating functionality in our applications into contexts. These generators are a great way to hit the ground running while Phoenix nudges you in the right direction to grow your application. Let's put these tools to use for our new user accounts context. +Phoenix includes the `mix phx.gen.html`, `mix phx.gen.json`, `mix phx.gen.live`, and `mix phx.gen.context` generators that apply the ideas of isolating functionality in our applications into contexts. These generators are a great way to hit the ground running while Phoenix nudges you in the right direction to grow your application. Let's put these tools to use for our new user accounts context. 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 users of our product. Along with users comes things like account login credentials and user registration. An `Accounts` context in our system is a natural place for our user functionality to live. @@ -48,7 +48,7 @@ The database for Hello.Repo has been created 14:38:37.418 [info] Already up ``` -Now we're ready to create our accounts context. We'll use the `phx.gen.html` task which creates a context module that wraps up Ecto access for creating, updating, and deleting users, along with web files like controllers and templates for the web interface into our context. Run the following command at your project root: +Now we're ready to create our accounts context. We'll use `mix phx.gen.html` which creates a context module that wraps up Ecto access for creating, updating, and deleting users, along with web files like controllers and templates for the web interface into our context. Run the following command at your project root: ```console $ mix phx.gen.html Accounts User users name:string \ @@ -125,7 +125,7 @@ If we follow the "Back" link, we get a list of all users, which should contain t ## Starting With Generators -That little `phx.gen.html` command packed a surprising punch. We got a lot of functionality out-of-the-box for creating, updating, and deleting users. This is far from a full-featured app, but remember, generators are first and foremost learning tools and a starting point for you to begin building real features. Code generation can't solve all your problems, but it will teach you the ins and outs of Phoenix and nudge you towards the proper mind-set when designing your application. +That little `mix phx.gen.html` command packed a surprising punch. We got a lot of functionality out-of-the-box for creating, updating, and deleting users. This is far from a full-featured app, but remember, generators are first and foremost learning tools and a starting point for you to begin building real features. Code generation can't solve all your problems, but it will teach you the ins and outs of Phoenix and nudge you towards the proper mind-set when designing your application. Let's first check out the `UserController` that was generated in `lib/hello_web/controllers/user_controller.ex`: @@ -246,7 +246,7 @@ defmodule Hello.Accounts.User do end ``` -This is just what we saw before when we ran the `mix phx.gen.schema` task, except here we see a `@doc false` above our `changeset/2` function. This tells us that while this function is publicly callable, it's not part of the public context API. Callers that build changesets do so via the context API. For example, `Accounts.create_user/1` calls into our `User.changeset/2` to build the changeset from user input. Callers, such as our controller actions, do not access `User.changeset/2` directly. All interaction with our user changesets is done through the public `Accounts` context. +This is just what we saw before when we ran `mix phx.gen.schema`, except here we see a `@doc false` above our `changeset/2` function. This tells us that while this function is publicly callable, it's not part of the public context API. Callers that build changesets do so via the context API. For example, `Accounts.create_user/1` calls into our `User.changeset/2` to build the changeset from user input. Callers, such as our controller actions, do not access `User.changeset/2` directly. All interaction with our user changesets is done through the public `Accounts` context. ## In-context Relationships @@ -270,7 +270,7 @@ Remember to update your repository by running migrations: $ mix ecto.migrate ``` -This time around, we used the `phx.gen.context` task, which is just like `phx.gen.html`, except it doesn't generate the web files for us. Since we already have controllers and templates for managing users, we can integrate the new credential features into our existing web form. +This time around, we used `mix phx.gen.context`, which is just like `mix phx.gen.html`, except it doesn't generate the web files for us. Since we already have controllers and templates for managing users, we can integrate the new credential features into our existing web form. We can see from the output that Phoenix generated an `accounts/credential.ex` file for our `Accounts.Credential` schema, as well as a migration. Notably, phoenix said it was `* injecting` code into the existing `accounts.ex` context file and test file. Since our `Accounts` module already exists, Phoenix knows to inject our code here. @@ -683,7 +683,7 @@ Generated hello app Now, let's fire up the server with `mix phx.server` and visit [http://localhost:4000/cms/pages](http://localhost:4000/cms/pages). If we haven't logged in yet, we'll be redirected to the home page with a flash error message telling us to sign in. Let's sign in at [http://localhost:4000/sessions/new](http://localhost:4000/sessions/new), then re-visit [http://localhost:4000/cms/pages](http://localhost:4000/cms/pages). Now that we're authenticated, we should see a familiar resource listing for pages, with a `New Page` link. -Before we create any pages, we need page authors. Let's run the `phx.gen.context` generator to generate an `Author` schema along with injected context functions: +Before we create any pages, we need page authors. Let's run the `mix phx.gen.context` generator to generate an `Author` schema along with injected context functions: ``` $ mix phx.gen.context CMS Author authors bio:text role:string \ diff --git a/guides/routing.md b/guides/routing.md index ea9f18a790..e2255a0f19 100644 --- a/guides/routing.md +++ b/guides/routing.md @@ -53,7 +53,7 @@ get "/", PageController, :index ## Examining Routes -Phoenix provides a great tool for investigating routes in an application, the mix task `phx.routes`. +Phoenix provides a great tool for investigating routes in an application: `mix phx.routes`. Let's see how this works. Go to the root of a newly-generated Phoenix application and run `mix phx.routes`. You should see something like the following, generated with all routes you currently have: @@ -165,7 +165,7 @@ Using path helpers makes it easy to ensure our controllers, views and templates ### More on Path Helpers -When we ran the `phx.routes` task for our user resource, it listed the `user_path` as the path helper function for each line of output. Here is what that translates to for each action: +When we ran `mix phx.routes` for our user resource, it listed the `user_path` as the path helper function for each line of output. Here is what that translates to for each action: ```elixir iex> alias HelloWeb.Router.Helpers, as: Routes diff --git a/guides/testing/testing_controllers.md b/guides/testing/testing_controllers.md index 72173977ff..965093f877 100644 --- a/guides/testing/testing_controllers.md +++ b/guides/testing/testing_controllers.md @@ -232,7 +232,7 @@ Finished in 0.6 seconds Randomized with seed 618478 ``` -You may have noticed that this time the scaffold controller has generated fewer tests. Previously it generated 16 (we went from 3 to 19) and now it generated 14 (we went from 19 to 33). That's because JSON APIs do not need to expose the `new` and `edit` actions. We can see this is the case in the resource we have added to the router at the end of the `phx.gen.json` command: +You may have noticed that this time the scaffold controller has generated fewer tests. Previously it generated 16 (we went from 3 to 19) and now it generated 14 (we went from 19 to 33). That's because JSON APIs do not need to expose the `new` and `edit` actions. We can see this is the case in the resource we have added to the router at the end of the `mix phx.gen.json` command: ```elixir resources "/articles", ArticleController, except: [:new, :edit] From 8a1d08260483d1f823fabc64b8674751a211558a Mon Sep 17 00:00:00 2001 From: Tyler Stromberg Date: Fri, 24 Apr 2020 23:20:02 -0700 Subject: [PATCH 008/199] Fix link in testing guide (#3791) --- guides/testing/testing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/testing/testing.md b/guides/testing/testing.md index 4b55290a04..3a1664d787 100644 --- a/guides/testing/testing.md +++ b/guides/testing/testing.md @@ -498,7 +498,7 @@ That's all you need to do and ExUnit and Phoenix will take care of all rest, inc ## Going further -While ExUnit is a simple test framework, it provides a really flexible and robust test runner through the `mix test` command. We recommend you to run `mix help test` or [read the docs online](https://hex) +While ExUnit is a simple test framework, it provides a really flexible and robust test runner through the `mix test` command. We recommend you to run `mix help test` or [read the docs online](https://hexdocs.pm/mix/Mix.Tasks.Test.html) We've seen what Phoenix gives us with a newly generated app. Furthermore, whenever you generate a new resource, Phoenix will generate all appropariate tests for that resource too. For example, you can create a complete scaffold with schema, context, controllers, and views by running the following command at the root of your application: From d812cfdb2357ae1846ee6435e74a3caa2da3ac28 Mon Sep 17 00:00:00 2001 From: onionjake Date: Sat, 25 Apr 2020 00:20:34 -0600 Subject: [PATCH 009/199] guides: minor typo: insided -> inside (#3793) --- guides/plug.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/plug.md b/guides/plug.md index 33c1905b0e..eccc069d6f 100644 --- a/guides/plug.md +++ b/guides/plug.md @@ -171,7 +171,7 @@ This block is only executed in development. It enables live reloading (if you ch ### Router plugs -In the router, we can declare plugs insided pipelines: +In the router, we can declare plugs inside pipelines: ```elixir defmodule HelloWeb.Router do From 4853b9310fb0f4e279e1c3835336bb301c301cdb Mon Sep 17 00:00:00 2001 From: Tobias Schoknecht Date: Sun, 26 Apr 2020 08:15:59 +0200 Subject: [PATCH 010/199] Fix minor typo in phoenix/guides/routing.md (#3794) Corrects `rasied` to `raised` in `If no route matches, no pipeline is invoked and a 404 error is raised.` of the pipeline description --- guides/routing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/routing.md b/guides/routing.md index e2255a0f19..2db728c840 100644 --- a/guides/routing.md +++ b/guides/routing.md @@ -537,7 +537,7 @@ Let's say that the request matches our first route: a GET to `/`. The router wil Conversely, if the request matches any of the routes defined by the `resources/2` macro, the router will pipe it through the `:api` pipeline - which currently does nothing - before it dispatches further to the correct action of the `HelloWeb.ReviewController`. -If no route matches, no pipeline is invoked and a 404 error is rasied. +If no route matches, no pipeline is invoked and a 404 error is raised. Let's stretch these ideas out a little bit more. What if we need to pipe requests through both `:browser` and one or more custom pipelines? We simply `pipe_through` a list of pipelines, and Phoenix will invoke them in order. From b3b5f2396b914fea741264881ea951897d3c5cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Gibowski?= Date: Sun, 26 Apr 2020 08:20:20 +0100 Subject: [PATCH 011/199] Fix grammar in plug guide (#3795) --- guides/plug.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/plug.md b/guides/plug.md index eccc069d6f..091b668bc4 100644 --- a/guides/plug.md +++ b/guides/plug.md @@ -290,7 +290,7 @@ defmodule HelloWeb.MessageController do end ``` -To make this all work, we converted the nested blocks of code and used `halt(conn)` whenever we reached a failure path. The `halt(conn)` functionality is essential here: it tells Plug that the next plug should not invoked. +To make this all work, we converted the nested blocks of code and used `halt(conn)` whenever we reached a failure path. The `halt(conn)` functionality is essential here: it tells Plug that the next plug should not be invoked. At the end of the day, by replacing the nested blocks of code with a flattened series of plug transformations, we are able to achieve the same functionality in a much more composable, clear, and reusable way. From f46218277f7a932eaaa625125dcc7d2f55250200 Mon Sep 17 00:00:00 2001 From: Okke Tijhuis Date: Sun, 26 Apr 2020 09:19:47 +0200 Subject: [PATCH 012/199] improve docker example (#3796) - add python (needed for nodegyp rebuild, will display error otherwise) - mkdir isn't needed, WORKDIR will create the directory if it doesn't exist - combine commands where possible to reduce the amount of docker layers - copy package*.json and install packages before doing the deploy so the install won't happen every time you modify a js/css file or other asset - bash not needed, sh/ash is available - install ncurses-libs, the app wouldn't start otherwise - do the chown when /app is still empty and add chown to the copy command, this saves docker from creating a large layer (about 10Mb) just for the permission changes - add a default cmd - use apk --no-cache so the cache doesn't get added to the image --- guides/deployment/releases.md | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/guides/deployment/releases.md b/guides/deployment/releases.md index f76fc8bfd3..dfaef3bf8b 100644 --- a/guides/deployment/releases.md +++ b/guides/deployment/releases.md @@ -175,10 +175,9 @@ Here is an example Docker file to run at the root of your application covering a FROM elixir:1.9.0-alpine AS build # install build dependencies -RUN apk add --update build-base npm git +RUN apk add --no-cache build-base npm git python # prepare build dir -RUN mkdir /app WORKDIR /app # install hex + rebar @@ -191,35 +190,38 @@ ENV MIX_ENV=prod # install mix dependencies COPY mix.exs mix.lock ./ COPY config config -RUN mix deps.get -RUN mix deps.compile +RUN mix do deps.get, deps.compile # build assets -COPY assets assets +COPY assets/package.json assets/package-lock.json ./assets/ +RUN npm --prefix ./assets ci --progress=false --no-audit --loglevel=error + COPY priv priv -RUN cd assets && npm install && npm run deploy +COPY assets assets +RUN npm run --prefix ./assets deploy RUN mix phx.digest -# build project +# compile and build release COPY lib lib -RUN mix compile - -# build release (uncomment COPY if rel/ exists) +# uncomment COPY if rel/ exists # COPY rel rel -RUN mix release +RUN mix do compile, release # prepare release image FROM alpine:3.9 AS app -RUN apk add --update bash openssl +RUN apk add --no-cache openssl ncurses-libs -RUN mkdir /app WORKDIR /app -COPY --from=build /app/_build/prod/rel/my_app ./ -RUN chown -R nobody: /app -USER nobody +RUN chown nobody:nobody /app + +USER nobody:nobody + +COPY --from=build --chown=nobody:nobody /app/_build/prod/rel/my_app ./ ENV HOME=/app + +CMD ["bin/my_app", "start"] ``` At the end, you will have an application in `/app` ready to run as `bin/my_app start`. From bedb896acaa13c8529b5fe49ca217f4e2db67d50 Mon Sep 17 00:00:00 2001 From: peter madsen Date: Mon, 27 Apr 2020 17:28:08 +0200 Subject: [PATCH 013/199] bump suggested node_version due to npm bug (#3801) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit node 10.19.0 was bitten by this bug "invalid bin entry" - failing on npm install (sic!) https://github.com/npm/cli/issues/613 see also https://elixirforum.com/t/cant-deploy-phoenix-app-to-heroku/31023 💙 does this need to be "backported" to 1.5 branch to appear on site? --- guides/deployment/heroku.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/deployment/heroku.md b/guides/deployment/heroku.md index 9a20bb9d89..5121bbfb89 100644 --- a/guides/deployment/heroku.md +++ b/guides/deployment/heroku.md @@ -119,7 +119,7 @@ The Phoenix Static buildpack uses a predefined Node version but to avoid surpris ``` # Node version -node_version=10.19.0 +node_version=10.20.1 ``` Please refer to the [configuration section](https://github.com/gjaldon/heroku-buildpack-phoenix-static#configuration) for full details. You can make your own custom build script, but for now we will use the [default one provided](https://github.com/gjaldon/heroku-buildpack-phoenix-static/blob/master/compile). From 675e65fa0c6341e63cba753d746e8beb08c60b1b Mon Sep 17 00:00:00 2001 From: Michael Crumm Date: Tue, 28 Apr 2020 21:46:49 -0700 Subject: [PATCH 014/199] Telemetry guide (#3768) A guide to telemetry events and the Telemetry supervisor introduced in Phoenix v1.5. Co-authored-by: Aaron Renner --- guides/telemetry.md | 585 ++++++++++++++++++++++++++++++++++++++++++++ mix.exs | 3 +- 2 files changed, 587 insertions(+), 1 deletion(-) create mode 100644 guides/telemetry.md diff --git a/guides/telemetry.md b/guides/telemetry.md new file mode 100644 index 0000000000..4386868d17 --- /dev/null +++ b/guides/telemetry.md @@ -0,0 +1,585 @@ +# Telemetry + +In this guide, we will show you how to instrument and report +on `:telemetry` events in your Phoenix application. + +> `te·lem·e·try` - the process of recording and transmitting +the readings of an instrument. + +As you follow along with this guide, we will introduce you to +the core concepts of Telemetry, you will initialize a +reporter to capture your application's events as they occur, +and we will guide you through the steps to properly +instrument your own functions using `:telemetry`. Let's take +a closer look at how Telemetry works in your application. + +## The Telemetry supervisor + +Since v1.5, new Phoenix applications are generated with a +Telemetry supervisor. This module is responsible for +managing the lifecycle of your Telemetry processes. It also +defines a `metrics/0` function, which returns a list of +[`Telemetry.Metrics`](https://hexdocs.pm/telemetry_metrics) +that you define for your application. + +By default, the supervisor also starts +[`:telemetry_poller`](http://hexdocs.pm/telemetry_poller). +By simply adding `:telemetry_poller` as a dependency, you +can receive VM-related events on a specified interval. + +If you are coming from an older version of Phoenix, install +the `:telemetry_metrics` and `:telemetry_poller` packages: + +```elixir +{:telemetry_metrics, "~> 0.4"}, +{:telemetry_poller, "~> 0.4"} +``` + +and create your Telemetry supervisor at +`lib/my_app_web/telemetry.ex`: + +```elixir +# lib/my_app_web/telemetry.ex +defmodule MyAppWeb.Telemetry do + use Supervisor + import Telemetry.Metrics + + def start_link(arg) do + Supervisor.start_link(__MODULE__, arg, name: __MODULE__) + end + + def init(_arg) do + children = [ + {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} + # Add reporters as children of your supervision tree. + # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} + ] + + Supervisor.init(children, strategy: :one_for_one) + end + + def metrics do + [ + # Phoenix Metrics + summary("phoenix.endpoint.stop.duration", + unit: {:native, :millisecond} + ), + summary("phoenix.router_dispatch.stop.duration", + tags: [:route], + unit: {:native, :millisecond} + ), + # VM Metrics + summary("vm.memory.total", unit: {:byte, :kilobyte}), + summary("vm.total_run_queue_lengths.total"), + summary("vm.total_run_queue_lengths.cpu"), + summary("vm.total_run_queue_lengths.io") + ] + end + + defp periodic_measurements do + [ + # A module, function and arguments to be invoked periodically. + # This function must call :telemetry.execute/3 and a metric must be added above. + # {MyApp, :count_users, []} + ] + end +end +``` + +Make sure to replace MyApp by your actual application name. + +Then add to your main application's supervision tree +(usually in `lib/my_app/application.ex`): + +```elixir +children = [ + MyApp.Repo, + MyAppWeb.Telemetry, + MyAppWeb.Endpoint, + ... +] +``` + +## Telemetry Events + +Many Elixir libraries (including Phoenix) are already using +the `:telemetry` package (http://hexdocs.pm/telemetry) as a +way to give users more insight into the behavior of their +applications, by emitting events at key moments in the +application lifecycle. + +A Telemetry event is made up of the following: + + * `name` - A string (e.g. `"my_app.worker.stop"`) or a + list of atoms that uniquely identifies the event. + + * `measurements` - A map of atom keys (e.g. `:duration`) + and numeric values. + + * `metadata` - A map of key/value pairs that can be used + for tagging metrics. + +### A Phoenix Example + +Here is an example of an event from your endpoint: + +* `[:phoenix, :endpoint, :stop]` - dispatched by + `Plug.Telemetry` in your endpoint whenever the response is + sent + + * Measurement: `%{duration: native_time}` + + * Metadata: `%{conn: Plug.Conn.t}` + +This means that after each request, `Plug`, via `:telemetry`, +will emit a "stop" event, with a measurement of how long it +took to get the response: + +```elixir +:telemetry.execute([:phoenix, :endpoint, :stop], %{duration: duration}, %{conn: conn}) +``` + +## Metrics + +> Metrics are aggregations of Telemetry events with a +> specific name, providing a view of the system's behaviour +> over time. +> +> ― `Telemetry.Metrics` + +The Telemetry.Metrics package provides a common interface +for defining metrics. What this means is that the package +does not perform any aggregation of the measurements itself, +but instead relies on reporters to do so. We will discuss +reporters in the next section. + +Using `Telemetry.Metrics`, you can define a counter metric, +which counts how many HTTP requests were completed: + +```elixir +Telemetry.Metrics.counter("phoenix.endpoint.stop.duration") +``` + +or you could use a distribution metric to see how many +requests were completed in particular time buckets: + +```elixir +Telemetry.Metrics.distribution("phoenix.endpoint.stop.duration", buckets: [100, 200, 300]) +``` + +This ability to introspect HTTP requests is really powerful -- +and this is but one of _many_ telemetry events emitted by +the Phoenix framework! We'll discuss more of these events, +as well as specific patterns for extracting valuable data +from Phoenix/Plug events in the +[Phoenix Metrics](#phoenix-metrics) section later in this +guide. + +> The full list of `:telemetry` events emitted from Phoenix, +along with their measurements and metadata, is available in +the "Instrumentation" section of the `Phoenix.Logger` module +documentation. + +### An Ecto Example + +Like Phoenix, Ecto ships with built-in telemetry events. +This means that you can gain introspection into your web +and database layers using the same tools. + +For instance, you might want to graph query execution time: + +```elixir +Telemetry.Metrics.summary("my_app.repo.query.query_time", + unit: {:native, :millisecond} +) +``` + +or view how long queries spend queued: + +```elixir +Telemetry.Metrics.distribution("my_app.repo.query.queue_time", + unit: {:native, :millisecond}, + buckets: [10, 50, 100] +) +``` + +> You can learn more about Ecto Telemetry in the "Telemetry +Events" section of the +[`Ecto.Repo`](https://hexdocs.pm/ecto/Ecto.Repo.html) module +documentation. + +So far we have seen some of the Telemetry events common to +Phoenix applications, along with some examples of their +various measurements and metadata. With all of this data +just waiting to be consumed, let's talk about reporters. + +## Reporters + +Reporters subscribe to Telemetry events using the common +interface provided by `Telemetry.Metrics`. They then +aggregate the measurements (data) into metrics to provide +meaningful information about your application. + +### Phoenix.LiveDashboard + +For developers interested in real-time visualizations for +their Telemetry metrics, you may be interested in installing +[`LiveDashboard`](https://hexdocs.pm/phoenix_live_dashboard). +LiveDashboard acts as a Telemetry.Metrics reporter to render +your data as beautiful, real-time charts on the dashboard. + +### Telemetry.Metrics.ConsoleReporter + +`Telemetry.Metrics` ships with a `ConsoleReporter` that can +be used to print events and metrics to the terminal. You can +use this reporter to experiment with the metrics discussed in +this guide. + +Uncomment or add the following to this list of children in +your Telemetry supervision tree (usually in +`lib/my_app_web/telemetry.ex`): + +```elixir +{Telemetry.Metrics.ConsoleReporter, metrics: metrics()} +``` + +> There are numerous reporters available, for services like +> StatsD, Prometheus, and more. You can find them by +> searching for "telemetry_metrics" on [hex.pm](https://hex.pm/packages?search=telemetry_metrics). + +## Phoenix Metrics + +Earlier we looked at the "stop" event emitted by +`Plug.Telemetry`, and used it to count the number of HTTP +requests. In reality, it's only somewhat helpful to be +able to see just the total number of requests. What if you +wanted to see the number of requests per route, or per route +_and_ method? + +Let's take a look at another event emitted during the HTTP +request lifecycle, this time from `Phoenix.Router`: + +* `[:phoenix, :router_dispatch, :stop]` - dispatched by + Phoenix.Router after successfully dispatching to a matched + route + + * Measurement: `%{duration: native_time}` + + * Metadata: `%{conn: Plug.Conn.t, route: binary, plug: module, plug_opts: term, path_params: map, pipe_through: [atom]}` + +Let's start by grouping these events by route. Add the +following (if it does not already exist) to the `metrics/0` +function of your Telemetry supervisor (usually in +`lib/my_app_web/telemetry.ex`): + +```elixir +# lib/my_app_web/telemetry.ex +def metrics do + [ + ...metrics... + summary("phoenix.router_dispatch.stop.duration", + tags: [:route], + unit: {:native, :millisecond} + ) + ] +end +``` + +Restart your server, and then make requests to a page or two. +In your terminal, you should see the ConsoleReporter print +logs for the Telemetry events it received as a result of +the metrics definitions you provided. + +The log line for each request contains the specific route +for that request. This is due to specifying the `:tags` +option for the summary metric, which takes care of our first +requirement; we can use `:tags` to group metrics by route. +Note that reporters will necessarily handle tags differently +depending on the underlying service in use. + +Looking more closely at the Router "stop" event, you can see +that the `Plug.Conn` struct representing the request is +present in the metadata, but how do you access the +properties in `conn`? + +Fortunately, `Telemetry.Metrics` provides the following +options to help you classify your events: + +* `:tags` - A list of metadata keys for grouping; + +* `:tag_values` - A function which transforms the metadata + into the desired shape; Note that this function is called + for each event, so it's important to keep it fast if the + rate of events is high. + +> Learn about all the available metrics options in the +`Telemetry.Metrics` module documentation. + +Let's find out how to extract more tags from events that +include a `conn` in their metadata. + +### Extracting tag values from Plug.Conn + +Let's add another metric for the route event, this time to +group by route and method: + +```elixir +summary("phoenix.router_dispatch.stop.duration", + tags: [:method, :route], + tag_values: &get_and_put_http_method/1, + unit: {:native, :millisecond} +) +``` + +We've introduced the `:tag_values` option here, because we +need to perform a transformation on the event metadata in +order to get to the values we need. + +Add the following private function to your Telemetry module +to lift the `:method` value from the `Plug.Conn` struct: + +```elixir +# lib/my_app_web/telemetry.ex +defp get_and_put_http_method(%{conn: %{method: method}} = metadata) do + Map.put(metadata, :method, method) +end +``` + +Restart your server and make some more requests. You should +begin to see logs with tags for both the HTTP method and the +route. + +Note the `:tags` and `:tag_values` options can be applied to +all `Telemetry.Metrics` types. + +## Periodic measurements + +You might want to periodically measure key values within +your application. Fortunately the +[`:telemetry_poller`](http://hexdocs.pm/telemetry_poller) +package provides a mechanism for custom measurements, +which is useful for retrieving process information or for +performing custom measurements periodically. + +Add the following to the list in your Telemetry supervisor's +`periodic_measurements/0` function, which is a private +function that returns a list of measurements to take on a +specified interval. + +```elixir +# lib/my_app_web/telemetry.ex +defp periodic_measurements do + [ + {MyApp, :measure_users, []}, + {:process_info, + event: [:my_app, :my_server], + name: MyApp.MyServer, + keys: [:message_queue_len, :memory]} + ] +end +``` + +where `MyApp.measure_users/0` could be written like this: + +```elixir +# lib/my_app.ex +defmodule MyApp do + def measure_users do + :telemetry.execute([:my_app, :users], %{total: MyApp.users_count()}, %{}) + end +end +``` + +Now with measurements in place, you can define the metrics for the +events above: + +```elixir +# lib/my_app_web/telemetry.ex +def metrics do + [ + ...metrics... + # MyApp Metrics + last_value("my_app.users.total"), + last_value("my_app.my_server.memory", unit: :byte), + last_value("my_app.my_server.message_queue_len") + summary("my_app.my_server.call.stop.duration"), + counter("my_app.my_server.call.exception") + ] +end +``` + +> You will implement MyApp.MyServer in the +[Custom Events](#custom-events) section. + +## Libraries using Telemetry + +Telemetry is quickly becoming the de-facto standard for +package instrumentation in Elixir. Here is a list of +libraries currently emitting `:telemetry` events. + +Library authors are actively encouraged to send a PR adding +their own (in alphabetical order, please): + +* [Absinthe](https://hexdocs.pm/absinthe) - Coming Soon! +* [Broadway](https://hexdocs.pm/broadway) - [Events](https://hexdocs.pm/broadway/Broadway.html#module-telemetry) +* [Ecto](https://hexdocs.pm/ecto) - [Events](https://hexdocs.pm/ecto/Ecto.Repo.html#module-telemetry-events) +* [Oban](https://hexdocs.pm/oban) - [Events](https://hexdocs.pm/oban/Oban.Telemetry.html) +* [Phoenix](https://hexdocs.pm/phoenix) - [Events](https://hexdocs.pm/phoenix/Phoenix.Logger.html#module-instrumentation) +* [Plug](https://hexdocs.pm/plug) - [Events](https://hexdocs.pm/plug/Plug.Telemetry.html) +* [Tesla](https://hexdocs.pm/tesla) - [Events](https://hexdocs.pm/tesla/Tesla.Middleware.Telemetry.html) + +## Custom Events + +If you need custom metrics and instrumentation in your +application, you can utilize the `:telemetry` package +(https://hexdocs.pm/telemetry) just like your favorite +frameworks and libraries. + +Here is an example of a simple GenServer that emits telemetry +events. Create this file in your app at +`lib/my_app/my_server.ex`: + +```elixir +# lib/my_app/my_server.ex +defmodule MyApp.MyServer do + @moduledoc """ + An example GenServer that runs arbitrary functions and emits telemetry events when called. + """ + use GenServer + + # A common prefix for :telemetry events + @prefix [:my_app, :my_server, :call] + + def start_link(fun) do + GenServer.start_link(__MODULE__, fun, name: __MODULE__) + end + + @doc """ + Runs the function contained within this server. + + ## Events + + The following events may be emitted: + + * `[:my_app, :my_server, :call, :start]` - Dispatched + immediately before invoking the function. This event + is always emitted. + + * Measurement: `%{system_time: system_time}` + + * Metadata: `%{}` + + * `[:my_app, :my_server, :call, :stop]` - Dispatched + immediately after successfully invoking the function. + + * Measurement: `%{duration: native_time}` + + * Metadata: `%{}` + + * `[:my_app, :my_server, :call, :exception]` - Dispatched + immediately after invoking the function, in the event + the function throws or raises. + + * Measurement: `%{duration: native_time}` + + * Metadata: `%{kind: kind, reason: reason, stacktrace: stacktrace}` + """ + def call!, do: GenServer.call(__MODULE__, :called) + + @impl true + def init(fun) when is_function(fun, 0), do: {:ok, fun} + + @impl true + def handle_call(:called, _from, fun) do + # Wrap the function invocation in a "span" + result = telemetry_span(fun) + + {:reply, result, fun} + end + + # Emits telemetry events related to invoking the function + defp telemetry_span(fun) do + start_time = emit_start() + + try do + fun.() + catch + kind, reason -> + stacktrace = System.stacktrace() + duration = System.monotonic_time() - start_time + emit_exception(duration, kind, reason, stacktrace) + :erlang.raise(kind, reason, stacktrace) + else + result -> + duration = System.monotonic_time() - start_time + emit_stop(duration) + result + end + end + + defp emit_start do + start_time_mono = System.monotonic_time() + + :telemetry.execute( + @prefix ++ [:start], + %{system_time: System.system_time()}, + %{} + ) + + start_time_mono + end + + defp emit_stop(duration) do + :telemetry.execute( + @prefix ++ [:stop], + %{duration: duration}, + %{} + ) + end + + defp emit_exception(duration, kind, reason, stacktrace) do + :telemetry.execute( + @prefix ++ [:exception], + %{duration: duration}, + %{ + kind: kind, + reason: reason, + stacktrace: stacktrace + } + ) + end +end +``` + +and add it to your application's supervisor tree (usually in +`lib/my_app/application.ex`), giving it a function to invoke +when called: + +```elixir +# lib/my_app/application.ex +children = [ + # Start a server that greets the world + {MyApp.MyServer, fn -> "Hello, world!" end}, +] +``` + +Now start an IEx session and call the server: + +```elixir +iex(1)> MyApp.MyServer.call! +``` + +and you should see something like the following output: + +```elixir +[Telemetry.Metrics.ConsoleReporter] Got new event! +Event name: my_app.my_server.call.stop +All measurements: %{duration: 4000} +All metadata: %{} + +Metric measurement: #Function<2.111777250/1 in Telemetry.Metrics.maybe_convert_measurement/2> (summary) +With value: 0.004 millisecond +Tag values: %{} + +"Hello, world!" +``` diff --git a/mix.exs b/mix.exs index ea7cf38839..f4d78f79cc 100644 --- a/mix.exs +++ b/mix.exs @@ -81,7 +81,7 @@ defmodule Phoenix.MixProject do # Test dependencies {:gettext, "~> 0.15.0", only: :test}, {:telemetry_poller, "~> 0.4", only: :test}, - {:telemetry_metrics, "~> 0.4", only: :test}, + {:telemetry_metrics, "~> 0.4", only: [:docs, :test]}, {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test} ] end @@ -132,6 +132,7 @@ defmodule Phoenix.MixProject do "guides/ecto.md", "guides/contexts.md", "guides/mix_tasks.md", + "guides/telemetry.md", "guides/realtime/channels.md", "guides/realtime/presence.md", From 6d133c96accf016b1f2289b989442a0fbbbb4db1 Mon Sep 17 00:00:00 2001 From: Xavier Noria Date: Wed, 29 Apr 2020 20:49:22 +0200 Subject: [PATCH 015/199] Elaborate a bit more the intro to channels (#3800) --- guides/realtime/channels.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/guides/realtime/channels.md b/guides/realtime/channels.md index 222778381d..a6296f1ae0 100644 --- a/guides/realtime/channels.md +++ b/guides/realtime/channels.md @@ -14,8 +14,12 @@ Some possible use cases include: - Notifying a browser that a page's CSS or JavaScript has changed (this is handy in development) Conceptually, Channels are pretty simple. -Clients connect and subscribe to one or more topics, whether that's `public_chat` or `updates:user1`. -Any message sent on a topic, whether from the server or from a client, is sent to all clients subscribed to that topic (including the sender, if it's subscribed), like this: + +First, clients connect to the server using some transport, like WebSocket. Once connected, they join one or more topics. For example, to interact with a public chat room clients may join a topic called `public_chat`, and to receive updates from a product with ID 7, they may need to join a topic called `product_updates:7`. + +Clients can push messages to the topics they've joined, and can also receive messages from them. The other way around, Channel servers receive messages from their connected clients, and can push messages to them too. + +Servers are able to broadcast messages to all clients subscribed to a certain topic. This is illustrated in the following diagram: ```plaintext +----------------+ @@ -31,6 +35,8 @@ Any message sent on a topic, whether from the server or from a client, is sent t +----------------+ ``` +Broadcasts work even if the application runs on several nodes/computers. That is, if two clients have their socket connected to different application nodes and are subscribed to the same topic `T`, both of them will receive messages broadcasted to `T`. That is possible thanks to an internal PubSub mechanism. + Channels can support any kind of client: a browser, native app, smart watch, embedded device, or anything else that can connect to a network. All the client needs is a suitable library; see the [Client Libraries](#client-libraries) section below. Each client library communicates using one of the "transports" that Channels understand. From b2b59e681e59084409e2dd3abcf8406c329096d7 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 29 Apr 2020 20:48:02 +0200 Subject: [PATCH 016/199] Silence an "Already up" message during mix test (#3804) --- installer/templates/phx_single/mix.exs | 2 +- installer/templates/phx_umbrella/apps/app_name/mix.exs | 2 +- installer/templates/phx_umbrella/apps/app_name_web/mix.exs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/installer/templates/phx_single/mix.exs b/installer/templates/phx_single/mix.exs index 3618e1aea8..7bc051dbe8 100644 --- a/installer/templates/phx_single/mix.exs +++ b/installer/templates/phx_single/mix.exs @@ -65,7 +65,7 @@ defmodule <%= app_module %>.MixProject do setup: ["deps.get"<%= if ecto do %>, "ecto.setup"<% end %><%= if webpack do %>, "cmd npm install --prefix assets"<% end %>]<%= if ecto do %>, "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], - test: ["ecto.create --quiet", "ecto.migrate", "test"]<% end %> + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]<% end %> ] end end diff --git a/installer/templates/phx_umbrella/apps/app_name/mix.exs b/installer/templates/phx_umbrella/apps/app_name/mix.exs index eca153a086..3957bd1c89 100644 --- a/installer/templates/phx_umbrella/apps/app_name/mix.exs +++ b/installer/templates/phx_umbrella/apps/app_name/mix.exs @@ -51,7 +51,7 @@ defmodule <%= app_module %>.MixProject do setup: ["deps.get"<%= if ecto do %>, "ecto.setup"<% end %>]<%= if ecto do %>, "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], - test: ["ecto.create --quiet", "ecto.migrate", "test"]<% end %> + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]<% end %> ] end 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 e184f08ec8..2ea4511afb 100644 --- a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs +++ b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs @@ -59,7 +59,7 @@ defmodule <%= web_namespace %>.MixProject do defp aliases do [ setup: ["deps.get"<%= if webpack do %>, "cmd npm install --prefix assets"<% end %>]<%= if ecto do %>, - test: ["ecto.create --quiet", "ecto.migrate", "test"]<% end %> + test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]<% end %> ] end end From b4fa3e31d2198540cdda411382597a819fb900fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 30 Apr 2020 00:02:35 +0200 Subject: [PATCH 017/199] Compute cache static manifest hash and interpolate it whenever requested (#3808) --- installer/templates/phx_assets/app.js | 12 +- lib/phoenix/config.ex | 9 +- lib/phoenix/digester.ex | 175 ++++++++++-------- lib/phoenix/endpoint.ex | 10 +- lib/phoenix/endpoint/supervisor.ex | 6 +- .../digest/cleaner/cache_manifest.json | 3 +- ...latest_not_most_recent_cache_manifest.json | 3 +- .../{ => digest/compile}/cache_manifest.json | 3 +- .../compile}/cache_manifest_upgrade.json | 3 +- test/fixtures/digest/priv/static/app.js | 1 + test/phoenix/digester_test.exs | 158 +++++++++------- test/phoenix/endpoint/endpoint_test.exs | 8 +- 12 files changed, 229 insertions(+), 162 deletions(-) rename test/fixtures/{ => digest/compile}/cache_manifest.json (84%) rename test/fixtures/{ => digest/compile}/cache_manifest_upgrade.json (87%) diff --git a/installer/templates/phx_assets/app.js b/installer/templates/phx_assets/app.js index 7467d54e38..1722613222 100644 --- a/installer/templates/phx_assets/app.js +++ b/installer/templates/phx_assets/app.js @@ -17,8 +17,16 @@ import {Socket} from "phoenix" import NProgress from "nprogress" import {LiveSocket} from "phoenix_live_view" -let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") -let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) +// The _csrf_token is necessary to load the current session into the LiveView. +// The _cache_static_manifest_hash is used to detect whenever there is a new +// deploy and trigger a reload of the assets. The "PHOENIX_CACHE_STATIC_MANIFEST_HASH" +// will be automatically replaced by a hash by running `mix phx.digest` in prod. +let params = { + _csrf_token: document.querySelector("meta[name='csrf-token']").getAttribute("content"), + _cache_static_manifest_hash: "PHOENIX_CACHE_STATIC_MANIFEST_HASH" +} + +let liveSocket = new LiveSocket("/live", Socket, {params: params}) // Show progress bar on live navigation and form submits window.addEventListener("phx:page-loading-start", info => NProgress.start()) diff --git a/lib/phoenix/config.ex b/lib/phoenix/config.ex index 67dc513d71..570f3fe241 100644 --- a/lib/phoenix/config.ex +++ b/lib/phoenix/config.ex @@ -17,6 +17,13 @@ defmodule Phoenix.Config do GenServer.start_link(__MODULE__, {module, config, permanent}, opts) end + @doc """ + Puts a given key-value in config. + """ + def put_new(module, key, value) do + :ets.insert_new(module, {key, value}) + end + @doc """ Adds permanent configuration. @@ -115,7 +122,7 @@ defmodule Phoenix.Config do def init({module, config, permanent}) do :ets.new(module, [:named_table, :public, read_concurrency: true]) update(module, config, []) - :ets.insert(module, __config__: self()) + :ets.insert(module, {:__config__, self()}) {:ok, {module, [:__config__ | permanent]}} end diff --git a/lib/phoenix/digester.ex b/lib/phoenix/digester.ex index e50c945e07..a7ec275077 100644 --- a/lib/phoenix/digester.ex +++ b/lib/phoenix/digester.ex @@ -2,7 +2,7 @@ defmodule Phoenix.Digester do @digested_file_regex ~r/(-[a-fA-F\d]{32})/ @manifest_version 1 @empty_manifest %{ - "version" => 1, + "version" => @manifest_version, "digests" => %{}, "latest" => %{} } @@ -20,15 +20,16 @@ defmodule Phoenix.Digester do @spec compile(String.t(), String.t()) :: :ok | {:error, :invalid_path} def compile(input_path, output_path) do if File.exists?(input_path) do - unless File.exists?(output_path), do: File.mkdir_p!(output_path) + File.mkdir_p!(output_path) files = filter_files(input_path) - manifest = generate_manifest(files) - digested_files = add_digested_content(files, manifest) + latest = generate_latest(files) + hash = latest_hash(latest) digests = load_compile_digests(output_path) - save_manifest(digested_files, manifest, digests, output_path) + digested_files = Enum.map(files, &digested_contents(&1, latest, hash)) + save_manifest(digested_files, latest, hash, digests, output_path) Enum.each(digested_files, &write_to_disk(&1, output_path)) else {:error, :invalid_path} @@ -43,6 +44,16 @@ defmodule Phoenix.Digester do |> Enum.map(&map_file(&1, input_path)) end + defp generate_latest(files) do + Map.new( + files, + &{ + manifest_join(&1.relative_path, &1.filename), + manifest_join(&1.relative_path, &1.digested_filename) + } + ) + end + defp load_compile_digests(output_path) do manifest = load_manifest(output_path) manifest["digests"] @@ -64,43 +75,36 @@ defmodule Phoenix.Digester do defp migrate_manifest(%{"version" => @manifest_version} = manifest, _output_path), do: manifest defp migrate_manifest(_latest, _output_path), do: @empty_manifest - defp generate_manifest(files) do - Map.new( - files, - &{ - manifest_join(&1.relative_path, &1.filename), - manifest_join(&1.relative_path, &1.digested_filename) - } - ) - end - - defp add_digested_content(files, manifest) do - for file <- files do - %{file | digested_content: digested_contents(file, manifest)} - end - end - - defp save_manifest(files, latest, old_digests, output_path) do + defp save_manifest(files, latest, hash, old_digests, output_path) do old_digests_that_still_exist = old_digests |> Enum.filter(fn {file, _} -> File.exists?(Path.join(output_path, file)) end) |> Map.new() - new_digests = generate_new_digests(files) - digests = Map.merge(old_digests_that_still_exist, new_digests) - - save_manifest( - %{"latest" => latest, "version" => @manifest_version, "digests" => digests}, - output_path - ) + digests = Map.merge(old_digests_that_still_exist, generate_digests(files)) + write_manifest(latest, digests, hash, output_path) end - defp save_manifest(%{"latest" => _, "version" => _, "digests" => _} = manifest, output_path) do - manifest_content = Phoenix.json_library().encode!(manifest) + defp write_manifest(latest, digests, hash, output_path) do + json = %{ + "latest" => latest, + "version" => @manifest_version, + "digests" => digests, + "hash" => hash + } + + manifest_content = Phoenix.json_library().encode!(json) File.write!(Path.join(output_path, "cache_manifest.json"), manifest_content) end - defp generate_new_digests(files) do + defp latest_hash(latest) do + [@manifest_version, latest] + |> :erlang.term_to_binary() + |> :erlang.md5() + |> Base.url_encode64(padding: false) + end + + defp generate_digests(files) do Map.new( files, &{ @@ -161,7 +165,10 @@ defmodule Phoenix.Digester do :zlib.gzip(file.digested_content) ) - File.write!(Path.join(path, file.filename <> ".gz"), :zlib.gzip(file.content)) + File.write!( + Path.join(path, file.filename <> ".gz"), + :zlib.gzip(file.content) + ) end # uncompressed files @@ -172,57 +179,76 @@ defmodule Phoenix.Digester do end defp compress_file?(file) do - Path.extname(file.filename) in Application.get_env(:phoenix, :gzippable_exts) + Path.extname(file.filename) in Application.fetch_env!(:phoenix, :gzippable_exts) end - defp digested_contents(file, manifest) do - case Path.extname(file.filename) do - ".css" -> digest_stylesheet_asset_references(file, manifest) - ".js" -> digest_javascript_asset_references(file, manifest) - ".map" -> digest_javascript_map_asset_references(file, manifest) - _ -> file.content - end + defp digested_contents(file, latest, hash) do + ext = Path.extname(file.filename) + + file = + case ext do + ".js" -> %{file | content: digest_javascript_contents(file, hash)} + _ -> file + end + + digested_content = + case ext do + ".css" -> digest_stylesheet_asset_references(file, latest) + ".js" -> digest_javascript_asset_references(file, latest) + ".map" -> digest_javascript_map_asset_references(file, latest) + _ -> file.content + end + + %{file | digested_content: digested_content} + end + + @javascript_cache_manifest_hash_regex ~r{["'](PHOENIX_CACHE_STATIC_MANIFEST_HASH)["']} + + defp digest_javascript_contents(file, hash) do + Regex.replace(@javascript_cache_manifest_hash_regex, file.content, fn _, _ -> + "\"#{hash}\"" + end) end @stylesheet_url_regex ~r{(url\(\s*)(\S+?)(\s*\))} @quoted_text_regex ~r{\A(['"])(.+)\1\z} - defp digest_stylesheet_asset_references(file, manifest) do + defp digest_stylesheet_asset_references(file, latest) do Regex.replace(@stylesheet_url_regex, file.content, fn _, open, url, close -> case Regex.run(@quoted_text_regex, url) do [_, quote_symbol, url] -> - open <> quote_symbol <> digested_url(url, file, manifest, true) <> quote_symbol <> close + open <> quote_symbol <> digested_url(url, file, latest, true) <> quote_symbol <> close nil -> - open <> digested_url(url, file, manifest, true) <> close + open <> digested_url(url, file, latest, true) <> close end end) end @javascript_source_map_regex ~r{(//#\s*sourceMappingURL=\s*)(\S+)} - defp digest_javascript_asset_references(file, manifest) do + defp digest_javascript_asset_references(file, latest) do Regex.replace(@javascript_source_map_regex, file.content, fn _, source_map_text, url -> - source_map_text <> digested_url(url, file, manifest, false) + source_map_text <> digested_url(url, file, latest, false) end) end @javascript_map_file_regex ~r{(['"]file['"]:['"])([^,"']+)(['"])} - defp digest_javascript_map_asset_references(file, manifest) do + defp digest_javascript_map_asset_references(file, latest) do Regex.replace(@javascript_map_file_regex, file.content, fn _, open_text, url, close_text -> - open_text <> digested_url(url, file, manifest, false) <> close_text + open_text <> digested_url(url, file, latest, false) <> close_text end) end - defp digested_url("/" <> relative_path, _file, manifest, with_vsn?) do - case Map.fetch(manifest, relative_path) do + defp digested_url("/" <> relative_path, _file, latest, with_vsn?) do + case Map.fetch(latest, relative_path) do {:ok, digested_path} -> relative_digested_path(digested_path, with_vsn?) :error -> "/" <> relative_path end end - defp digested_url(url, file, manifest, with_vsn?) do + defp digested_url(url, file, latest, with_vsn?) do case URI.parse(url) do %URI{scheme: nil, host: nil} -> manifest_path = @@ -231,7 +257,7 @@ defmodule Phoenix.Digester do |> Path.expand() |> Path.relative_to_cwd() - case Map.fetch(manifest, manifest_path) do + case Map.fetch(latest, manifest_path) do {:ok, digested_path} -> absolute_digested_url(url, digested_path, with_vsn?) @@ -247,22 +273,20 @@ defmodule Phoenix.Digester do defp relative_digested_path(digested_path, true), do: relative_digested_path(digested_path) <> "?vsn=d" - defp relative_digested_path(digested_path, false), do: relative_digested_path(digested_path) - defp relative_digested_path(digested_path), do: "/" <> digested_path + defp relative_digested_path(digested_path, false), + do: relative_digested_path(digested_path) - defp absolute_digested_url(url, digested_path, true) do - absolute_digested_url(url, digested_path) <> "?vsn=d" - end + defp relative_digested_path(digested_path), + do: "/" <> digested_path - defp absolute_digested_url(url, digested_path, false) do - absolute_digested_url(url, digested_path) - end + defp absolute_digested_url(url, digested_path, true), + do: absolute_digested_url(url, digested_path) <> "?vsn=d" - defp absolute_digested_url(url, digested_path) do - url - |> Path.dirname() - |> Path.join(Path.basename(digested_path)) - end + defp absolute_digested_url(url, digested_path, false), + do: absolute_digested_url(url, digested_path) + + defp absolute_digested_url(url, digested_path), + do: url |> Path.dirname() |> Path.join(Path.basename(digested_path)) @doc """ Deletes compiled/compressed asset files that are no longer in use based on @@ -278,19 +302,18 @@ defmodule Phoenix.Digester do @spec clean(String.t(), integer, integer, integer) :: :ok | {:error, :invalid_path} def clean(path, age, keep, now \\ now()) do if File.exists?(path) do - manifest = load_manifest(path) - files = files_to_clean(manifest, now - age, keep) + %{"latest" => latest, "digests" => digests} = load_manifest(path) + files = files_to_clean(latest, digests, now - age, keep) remove_files(files, path) - remove_files_from_manifest(manifest, files, path) + write_manifest(latest, Map.drop(digests, files), latest_hash(latest), path) :ok else {:error, :invalid_path} end end - defp files_to_clean(manifest, max_age, keep) do - latest = Map.values(manifest["latest"]) - digests = Map.drop(manifest["digests"], latest) + defp files_to_clean(latest, digests, max_age, keep) do + digests = Map.drop(digests, Map.values(latest)) for {_, versions} <- group_by_logical_path(digests), file <- versions_to_clean(versions, max_age, keep), @@ -302,9 +325,7 @@ defmodule Phoenix.Digester do |> Enum.map(fn {path, attrs} -> Map.put(attrs, "path", path) end) |> Enum.sort_by(& &1["mtime"], &>/2) |> Enum.with_index(1) - |> Enum.filter(fn {version, index} -> - max_age > version["mtime"] || index > keep - end) + |> Enum.filter(fn {version, index} -> max_age > version["mtime"] || index > keep end) |> Enum.map(fn {version, _index} -> version["path"] end) end @@ -323,10 +344,4 @@ defmodule Phoenix.Digester do |> File.rm() end end - - defp remove_files_from_manifest(manifest, files, output_path) do - manifest - |> Map.update!("digests", &Map.drop(&1, files)) - |> save_manifest(output_path) - end end diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex index 25104f2107..699ae413d4 100644 --- a/lib/phoenix/endpoint.ex +++ b/lib/phoenix/endpoint.ex @@ -99,9 +99,13 @@ defmodule Phoenix.Endpoint do * `:cache_static_manifest` - a path to a json manifest file that contains static files and their digested version. This is typically set to "priv/static/cache_manifest.json" which is the file automatically generated - by `mix phx.digest`. - It can be either: a string containing a file system path or a tuple containing - the application name and the path within that application. + by `mix phx.digest`. It can be either: a string containing a file system path + or a tuple containing the application name and the path within that application. + + * `:cache_static_manifest_hash` - the hash of the cache static manifest. + This is automatically loaded from `cache_static_manifest` on boot. + However, if you have your own static handling mechanism, you may want to + set this value explicitly. * `:check_origin` - configure the default `:check_origin` setting for transports. See `socket/3` for options. Defaults to `true`. diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex index 00ae023cc8..69801ff9d4 100644 --- a/lib/phoenix/endpoint/supervisor.ex +++ b/lib/phoenix/endpoint/supervisor.ex @@ -78,7 +78,7 @@ defmodule Phoenix.Endpoint.Supervisor do end extra_conf = [ - endpoint_id: :crypto.strong_rand_bytes(16) |> Base.encode64, + endpoint_id: :crypto.strong_rand_bytes(16) |> Base.encode64(padding: false), # TODO: Remove this once :pubsub is removed pubsub_server: secret_conf[:pubsub_server] || secret_conf[:pubsub][:name] ] @@ -431,7 +431,9 @@ defmodule Phoenix.Endpoint.Supervisor do endpoint.static_path("/") end - defp warmup_static(endpoint, %{"latest" => latest, "digests" => digests}) do + defp warmup_static(endpoint, %{"latest" => latest, "digests" => digests} = manifest) do + Phoenix.Config.put_new(endpoint, :cache_static_manifest_hash, manifest["hash"]) + Enum.each(latest, fn {key, _} -> Phoenix.Config.cache(endpoint, {:__phoenix_static__, "/" <> key}, fn _ -> {:cache, static_cache(digests, Map.get(latest, key))} diff --git a/test/fixtures/digest/cleaner/cache_manifest.json b/test/fixtures/digest/cleaner/cache_manifest.json index 3c3c7b7b63..68155e0cba 100644 --- a/test/fixtures/digest/cleaner/cache_manifest.json +++ b/test/fixtures/digest/cleaner/cache_manifest.json @@ -22,5 +22,6 @@ "latest": { "app.css": "app-3.css" }, - "version": 1 + "version": 1, + "hash": "AxLYL9wnimiMdf3_NDnYvw" } diff --git a/test/fixtures/digest/cleaner/latest_not_most_recent_cache_manifest.json b/test/fixtures/digest/cleaner/latest_not_most_recent_cache_manifest.json index 78ace07d6c..60e7c08d90 100644 --- a/test/fixtures/digest/cleaner/latest_not_most_recent_cache_manifest.json +++ b/test/fixtures/digest/cleaner/latest_not_most_recent_cache_manifest.json @@ -22,5 +22,6 @@ "latest": { "app.css": "app-3.css" }, - "version": 1 + "version": 1, + "hash": "AxLYL9wnimiMdf3_NDnYvw" } diff --git a/test/fixtures/cache_manifest.json b/test/fixtures/digest/compile/cache_manifest.json similarity index 84% rename from test/fixtures/cache_manifest.json rename to test/fixtures/digest/compile/cache_manifest.json index dc286f9c6e..140d1f4870 100644 --- a/test/fixtures/cache_manifest.json +++ b/test/fixtures/digest/compile/cache_manifest.json @@ -10,5 +10,6 @@ "latest": { "foo.css": "foo-d978852bea6530fcd197b5445ed008fd.css" }, - "version": 1 + "version": 1, + "hash": "cjkuB6uDZecddGSFz7D7kg" } diff --git a/test/fixtures/cache_manifest_upgrade.json b/test/fixtures/digest/compile/cache_manifest_upgrade.json similarity index 87% rename from test/fixtures/cache_manifest_upgrade.json rename to test/fixtures/digest/compile/cache_manifest_upgrade.json index 23bce0f2f3..0a32d87280 100644 --- a/test/fixtures/cache_manifest_upgrade.json +++ b/test/fixtures/digest/compile/cache_manifest_upgrade.json @@ -16,5 +16,6 @@ "latest": { "foo.css": "foo-ghijkl.css" }, - "version": 1 + "version": 1, + "hash": "3xAzpSnmcgte3bMevWqJMA" } diff --git a/test/fixtures/digest/priv/static/app.js b/test/fixtures/digest/priv/static/app.js index 172e3a596b..0fa8d13429 100644 --- a/test/fixtures/digest/priv/static/app.js +++ b/test/fixtures/digest/priv/static/app.js @@ -1,4 +1,5 @@ (function() { + var digest = "PHOENIX_CACHE_STATIC_MANIFEST_HASH"; console.log('Hello World!'); })(); //# sourceMappingURL=app.js.map diff --git a/test/phoenix/digester_test.exs b/test/phoenix/digester_test.exs index 2d2d2c52b2..6eef0f5d05 100644 --- a/test/phoenix/digester_test.exs +++ b/test/phoenix/digester_test.exs @@ -2,7 +2,7 @@ defmodule Phoenix.DigesterTest do use ExUnit.Case, async: true @output_path Path.join("tmp", "phoenix_digest") - @fake_now 32132173 + @fake_now 32_132_173 @hash_regex ~S"[a-fA-F\d]{32}" setup do @@ -29,48 +29,60 @@ defmodule Phoenix.DigesterTest do assert "manifest.json" in output_files assert "manifest.json.gz" in output_files assert "cache_manifest.json" in output_files - assert Enum.any?(output_files, &(String.match?(&1, ~r/(phoenix-#{@hash_regex}\.png)/))) - refute Enum.any?(output_files, &(String.match?(&1, ~r/(phoenix-#{@hash_regex}\.png\.gz)/))) - - json = - Path.join(@output_path, "cache_manifest.json") - |> File.read!() - |> Phoenix.json_library().decode!() + assert Enum.any?(output_files, &String.match?(&1, ~r/(phoenix-#{@hash_regex}\.png)/)) + refute Enum.any?(output_files, &String.match?(&1, ~r/(phoenix-#{@hash_regex}\.png\.gz)/)) + json = Path.join(@output_path, "cache_manifest.json") |> json_read!() assert json["latest"]["phoenix.png"] =~ ~r"phoenix-#{@hash_regex}.png" assert json["version"] == 1 + assert byte_size(json["hash"]) == 22 end test "includes existing digests in new cache manifest" do source_path = "test/fixtures/digest/priv/static/" input_path = "tmp/digest/static" + File.rm_rf!(input_path) :ok = File.mkdir_p!(@output_path) :ok = File.mkdir_p!(input_path) + {:ok, _} = File.cp_r(source_path, input_path) - :ok = File.cp(Path.join(source_path, "foo.css"), Path.join(@output_path, "foo-d978852bea6530fcd197b5445ed008fd.css")) - :ok = File.cp("test/fixtures/cache_manifest.json", Path.join(@output_path, "cache_manifest.json")) + + :ok = + File.cp( + Path.join(source_path, "foo.css"), + Path.join(@output_path, "foo-d978852bea6530fcd197b5445ed008fd.css") + ) + + :ok = + File.cp( + "test/fixtures/digest/compile/cache_manifest.json", + Path.join(@output_path, "cache_manifest.json") + ) assert :ok = Phoenix.Digester.compile(input_path, @output_path) - json = - Path.join(@output_path, "cache_manifest.json") - |> File.read!() - |> Phoenix.json_library().decode!() + json = Path.join(@output_path, "cache_manifest.json") |> json_read!() # Keep old entries - assert json["digests"]["foo-d978852bea6530fcd197b5445ed008fd.css"]["logical_path"] == "foo.css" + assert json["digests"]["foo-d978852bea6530fcd197b5445ed008fd.css"]["logical_path"] == + "foo.css" + # Update mtime - assert_in_delta json["digests"]["foo-d978852bea6530fcd197b5445ed008fd.css"]["mtime"], now(), 2 + assert_in_delta json["digests"]["foo-d978852bea6530fcd197b5445ed008fd.css"]["mtime"], + now(), + 2 # Add new entries key = Enum.find(Map.keys(json["digests"]), &(&1 =~ ~r"phoenix-#{@hash_regex}.png")) - assert json["digests"][key]["logical_path"] == "phoenix.png" + assert json["version"] == 1 assert is_integer(json["digests"][key]["mtime"]) + assert json["digests"][key]["logical_path"] == "phoenix.png" assert json["digests"][key]["size"] == 13900 assert json["digests"][key]["digest"] =~ ~r"#{@hash_regex}" - assert json["digests"][key]["sha512"] == "93pY5dBa8nHHi0Zfj75O/vXCBXb+UvEVCyU7Yd3pzOJ7o1wkYBWbvs3pVXhBChEmo8MDANT11vsggo2+bnYqoQ==" - assert json["version"] == 1 + + assert json["digests"][key]["sha512"] == + "93pY5dBa8nHHi0Zfj75O/vXCBXb+UvEVCyU7Yd3pzOJ7o1wkYBWbvs3pVXhBChEmo8MDANT11vsggo2+bnYqoQ==" end test "old versions maintain their mtime" do @@ -79,35 +91,47 @@ defmodule Phoenix.DigesterTest do File.rm_rf!(input_path) :ok = File.mkdir_p!(@output_path) :ok = File.mkdir_p!(input_path) - :ok = File.cp(Path.join(source_path, "foo.css"), Path.join(@output_path, "foo-d978852bea6530fcd197b5445ed008fd.css")) - :ok = File.cp("test/fixtures/cache_manifest.json", Path.join(@output_path, "cache_manifest.json")) + + :ok = + File.cp( + Path.join(source_path, "foo.css"), + Path.join(@output_path, "foo-d978852bea6530fcd197b5445ed008fd.css") + ) + + :ok = + File.cp( + "test/fixtures/digest/compile/cache_manifest.json", + Path.join(@output_path, "cache_manifest.json") + ) + File.write!(Path.join(input_path, "foo.css"), ".foo { background-color: blue }") assert :ok = Phoenix.Digester.compile(input_path, @output_path) + json = Path.join(@output_path, "cache_manifest.json") |> json_read!() - json = - Path.join(@output_path, "cache_manifest.json") - |> File.read!() - |> Phoenix.json_library().decode!() + assert json["digests"]["foo-d978852bea6530fcd197b5445ed008fd.css"]["mtime"] == 32_132_171 - assert json["digests"]["foo-d978852bea6530fcd197b5445ed008fd.css"]["mtime"] == 32132171 - assert_in_delta json["digests"]["foo-1198fd3c7ecf0e8f4a33a6e4fc5ae168.css"]["mtime"], now(), 2 + assert_in_delta json["digests"]["foo-1198fd3c7ecf0e8f4a33a6e4fc5ae168.css"]["mtime"], + now(), + 2 end test "excludes files that no longer exist from cache manifest" do input_path = "tmp/digest/static" - File.rm_rf! input_path + File.rm_rf!(input_path) :ok = File.mkdir_p!(input_path) - :ok = File.cp("test/fixtures/cache_manifest.json", Path.join(input_path, "cache_manifest.json")) - assert :ok = Phoenix.Digester.compile(input_path, input_path) + :ok = + File.cp( + "test/fixtures/digest/compile/cache_manifest.json", + Path.join(input_path, "cache_manifest.json") + ) - json = - Path.join(input_path, "cache_manifest.json") - |> File.read!() - |> Phoenix.json_library().decode!() + assert :ok = Phoenix.Digester.compile(input_path, input_path) + json = Path.join(input_path, "cache_manifest.json") |> json_read!() assert json["digests"] == %{} + assert byte_size(json["hash"]) == 22 end test "digests and compress nested files" do @@ -119,13 +143,10 @@ defmodule Phoenix.DigesterTest do assert "static/phoenix.png" in output_files refute "static/phoenix.png.gz" in output_files assert "cache_manifest.json" in output_files - assert Enum.any?(output_files, &(String.match?(&1, ~r/(phoenix-#{@hash_regex}\.png)/))) - refute Enum.any?(output_files, &(String.match?(&1, ~r/(phoenix-#{@hash_regex}\.png\.gz)/))) + assert Enum.any?(output_files, &String.match?(&1, ~r/(phoenix-#{@hash_regex}\.png)/)) + refute Enum.any?(output_files, &String.match?(&1, ~r/(phoenix-#{@hash_regex}\.png\.gz)/)) - json = - Path.join(@output_path, "cache_manifest.json") - |> File.read!() - |> Phoenix.json_library().decode!() + json = Path.join(@output_path, "cache_manifest.json") |> json_read!() assert json["latest"]["static/phoenix.png"] =~ ~r"static/phoenix-#{@hash_regex}\.png" end @@ -140,15 +161,16 @@ defmodule Phoenix.DigesterTest do File.write!(input_file, "console.log('test');") assert :ok = Phoenix.Digester.compile(input_path, @output_path) + json1 = Path.join(@output_path, "cache_manifest.json") |> json_read!() + assert Enum.count(json1["digests"]) == 1 + File.write!(input_file, "console.log('test2');") assert :ok = Phoenix.Digester.compile(input_path, @output_path) - json = - Path.join(@output_path, "cache_manifest.json") - |> File.read!() - |> Phoenix.json_library().decode!() + json2 = Path.join(@output_path, "cache_manifest.json") |> json_read!() + assert Enum.count(json2["digests"]) == 2 - assert Enum.count(json["digests"]) == 2 + assert json1["hash"] != json2["hash"] end test "doesn't duplicate files when digesting and compressing twice" do @@ -160,13 +182,15 @@ defmodule Phoenix.DigesterTest do File.write!(input_file, "console.log('test');") assert :ok = Phoenix.Digester.compile(input_path, input_path) + json1 = Path.join(input_path, "cache_manifest.json") |> json_read!() assert :ok = Phoenix.Digester.compile(input_path, input_path) + json2 = Path.join(input_path, "cache_manifest.json") |> json_read!() + assert json1["hash"] == json2["hash"] output_files = assets_files(input_path) - refute "file.js.gz.gz" in output_files refute "cache_manifest.json.gz" in output_files - refute Enum.any?(output_files, & &1 =~ ~r/file-#{@hash_regex}.[\w|\d]*.[-#{@hash_regex}/) + refute Enum.any?(output_files, &(&1 =~ ~r/file-#{@hash_regex}.[\w|\d]*.[-#{@hash_regex}/)) end test "digests only absolute and relative asset paths found within stylesheets" do @@ -207,20 +231,15 @@ defmodule Phoenix.DigesterTest do |> assets_files() |> Enum.find(&(&1 =~ ~r"cache_manifest.json")) - json = - @output_path - |> Path.join(cache_manifest_file) - |> File.read!() - |> Phoenix.json_library().decode!() - + json = Path.join(@output_path, cache_manifest_file) |> json_read!() integrity = Base.encode64(:crypto.hash(:sha512, digested_css)) - assert json["digests"][digested_css_filename]["sha512"] == integrity end test "digests sourceMappingURL asset paths found within javascript source files" do input_path = "test/fixtures/digest/priv/static/" assert :ok = Phoenix.Digester.compile(input_path, @output_path) + json = Path.join(@output_path, "cache_manifest.json") |> json_read!() digested_js_map_filename = assets_files(@output_path) @@ -230,12 +249,15 @@ defmodule Phoenix.DigesterTest do assets_files(@output_path) |> Enum.find(&(&1 =~ ~r"app-#{@hash_regex}.js")) - digested_js = - Path.join(@output_path, digested_js_filename) - |> File.read!() + digested_js = Path.join(@output_path, digested_js_filename) |> File.read!() + refute digested_js =~ "app.js.map" + assert digested_js =~ "#{digested_js_map_filename}" + assert digested_js =~ "var digest = \"#{json["hash"]}\";" - refute digested_js =~ ~r"app.js.map" - assert digested_js =~ ~r"#{digested_js_map_filename}$" + js = Path.join(@output_path, "app.js") |> File.read!() + assert js =~ "app.js.map" + refute js =~ "#{digested_js_map_filename}" + assert js =~ "var digest = \"#{json["hash"]}\";" end test "digests file url paths found within javascript mapping files" do @@ -282,6 +304,7 @@ defmodule Phoenix.DigesterTest do manifest_path = "test/fixtures/digest/cleaner/cache_manifest.json" File.mkdir_p!(@output_path) File.cp(manifest_path, "#{@output_path}/cache_manifest.json") + File.touch("#{@output_path}/app.css") File.touch("#{@output_path}/app-1.css") File.touch("#{@output_path}/app-1.css.gz") @@ -381,12 +404,7 @@ defmodule Phoenix.DigesterTest do File.touch("#{@output_path}/app.css") assert :ok = Phoenix.Digester.clean(@output_path, 3600, 1, @fake_now) - - json = - Path.join(@output_path, "cache_manifest.json") - |> File.read!() - |> Phoenix.json_library().decode!() - + json = Path.join(@output_path, "cache_manifest.json") |> json_read!() refute json["digests"]["app-1.css"] end end @@ -394,12 +412,18 @@ defmodule Phoenix.DigesterTest do defp assets_files(path) do path |> Path.join("**/*") - |> Path.wildcard + |> Path.wildcard() |> Enum.filter(&(!File.dir?(&1))) - |> Enum.map(&(Path.relative_to(&1, path))) + |> Enum.map(&Path.relative_to(&1, path)) end defp now do :calendar.datetime_to_gregorian_seconds(:calendar.universal_time()) end + + defp json_read!(path) do + path + |> File.read!() + |> Phoenix.json_library().decode!() + end end diff --git a/test/phoenix/endpoint/endpoint_test.exs b/test/phoenix/endpoint/endpoint_test.exs index 87635dd083..79fe3c0fd8 100644 --- a/test/phoenix/endpoint/endpoint_test.exs +++ b/test/phoenix/endpoint/endpoint_test.exs @@ -8,7 +8,7 @@ defmodule Phoenix.Endpoint.EndpointTest do static_url: [host: "static.example.com"], server: false, http: [port: 80], https: [port: 443], force_ssl: [subdomains: true], - cache_static_manifest: "../../../../test/fixtures/cache_manifest.json", + cache_static_manifest: "../../../../test/fixtures/digest/compile/cache_manifest.json", pubsub_server: :endpoint_pub] Application.put_env(:phoenix, __MODULE__.Endpoint, @config) @@ -95,12 +95,14 @@ defmodule Phoenix.Endpoint.EndpointTest do end test "warms up caches on load and config change" do + assert Endpoint.config(:cache_static_manifest_hash) == "cjkuB6uDZecddGSFz7D7kg" assert Endpoint.static_path("/foo.css") == "/foo-d978852bea6530fcd197b5445ed008fd.css?vsn=d" # Trigger a config change and the cache should be warmed up again - config = put_in(@config[:cache_static_manifest], "../../../../test/fixtures/cache_manifest_upgrade.json") + config = put_in(@config[:cache_static_manifest], "../../../../test/fixtures/digest/compile/cache_manifest_upgrade.json") assert Endpoint.config_change([{Endpoint, config}], []) == :ok + assert Endpoint.config(:cache_static_manifest_hash) == "3xAzpSnmcgte3bMevWqJMA" assert Endpoint.static_path("/foo.css") == "/foo-ghijkl.css?vsn=d" end @@ -171,7 +173,7 @@ defmodule Phoenix.Endpoint.EndpointTest do end test "loads cache manifest from specified application" do - config = put_in(@config[:cache_static_manifest], {:phoenix, "../../../../test/fixtures/cache_manifest.json"}) + config = put_in(@config[:cache_static_manifest], {:phoenix, "../../../../test/fixtures/digest/compile/cache_manifest.json"}) assert Endpoint.config_change([{Endpoint, config}], []) == :ok assert Endpoint.static_path("/foo.css") == "/foo-d978852bea6530fcd197b5445ed008fd.css?vsn=d" From b935337a5e4ebb13fd6e5c77e98566666a0f83b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 30 Apr 2020 10:49:40 +0200 Subject: [PATCH 018/199] Update references on assigns --- installer/templates/phx_single/lib/app_name_web.ex | 3 ++- .../phx_umbrella/apps/app_name_web/lib/app_name.ex | 3 ++- lib/phoenix/view.ex | 14 ++++++++------ mix.lock | 2 +- 4 files changed, 13 insertions(+), 9 deletions(-) diff --git a/installer/templates/phx_single/lib/app_name_web.ex b/installer/templates/phx_single/lib/app_name_web.ex index e9213da99f..0433f15f2c 100644 --- a/installer/templates/phx_single/lib/app_name_web.ex +++ b/installer/templates/phx_single/lib/app_name_web.ex @@ -34,7 +34,8 @@ defmodule <%= web_namespace %> do namespace: <%= web_namespace %> # Import convenience functions from controllers - import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] + import Phoenix.Controller, + only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] # Include shared imports and aliases for views unquote(view_helpers()) diff --git a/installer/templates/phx_umbrella/apps/app_name_web/lib/app_name.ex b/installer/templates/phx_umbrella/apps/app_name_web/lib/app_name.ex index a7276714c1..40c4023218 100644 --- a/installer/templates/phx_umbrella/apps/app_name_web/lib/app_name.ex +++ b/installer/templates/phx_umbrella/apps/app_name_web/lib/app_name.ex @@ -34,7 +34,8 @@ defmodule <%= web_namespace %> do namespace: <%= web_namespace %> # Import convenience functions from controllers - import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] + import Phoenix.Controller, + only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] # Include shared imports and aliases for views unquote(view_helpers()) diff --git a/lib/phoenix/view.ex b/lib/phoenix/view.ex index 51c1d19a94..a6a32fb3a3 100644 --- a/lib/phoenix/view.ex +++ b/lib/phoenix/view.ex @@ -20,7 +20,8 @@ defmodule Phoenix.View do use Phoenix.View, root: "lib/your_app_web/templates", namespace: "web" # Import convenience functions from controllers - import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1] + import Phoenix.Controller, + only: [get_flash: 1, get_flash: 2, view_module: 1, view_template: 1] # Use all HTML functionality (forms, tags, etc) use Phoenix.HTML @@ -335,8 +336,9 @@ defmodule Phoenix.View do Renders a template only if it exists. Same as `render/3`, but returns `nil` instead of raising. - Useful for dynamically rendering templates in the layout that may or - may not be implemented by the `@view_module` view. + This is often used with `Phoenix.Controller.view_module/1` + and `Phoenix.Controller.view_template/1`, which must be + imported into your views. See the "Examples" section below. ## Examples @@ -345,10 +347,10 @@ defmodule Phoenix.View do may wish to inject certain scripts, while others will not. - <%= render_existing @view_module, "scripts.html", assigns %> + <%= render_existing view_module(@conn), "scripts.html", assigns %> - Then the module for the `@view_module` view can decide to provide scripts with + Then the module under `view_module(@conn)` can decide to provide scripts with either a precompiled template, or by implementing the function directly, ie: def render("scripts.html", _assigns) do @@ -366,7 +368,7 @@ defmodule Phoenix.View do `render_existing/3` for per-template based content, ie: - <%= render_existing @view_module, "scripts." <> @view_template, assigns %> + <%= render_existing view_module(@conn), "scripts." <> view_template(@conn), assigns %> def render("scripts.show.html", _assigns) do diff --git a/mix.lock b/mix.lock index e21275d6f2..f8ceb3aa37 100644 --- a/mix.lock +++ b/mix.lock @@ -10,7 +10,7 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, - "phoenix_html": {:hex, :phoenix_html, "2.14.0", "d8c6bc28acc8e65f8ea0080ee05aa13d912c8758699283b8d3427b655aabe284", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "b0bb30eda478a06dbfbe96728061a93833db3861a49ccb516f839ecb08493fbb"}, + "phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, "plug": {:hex, :plug, "1.10.0", "6508295cbeb4c654860845fb95260737e4a8838d34d115ad76cd487584e2fc4d", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "422a9727e667be1bf5ab1de03be6fa0ad67b775b2d84ed908f3264415ef29d4a"}, "plug_cowboy": {:hex, :plug_cowboy, "2.2.0", "858dab1b31b17b518270f02285975972a919fa412001e6ae7a199120b113ee24", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ffdbf993cc4e72726c677aafa9fd79bce1474dbfbe523be428d1ae5c7dbdf75b"}, From dbf0dda0610754c4267100f07c8964857eea4159 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Fri, 1 May 2020 14:29:31 +0200 Subject: [PATCH 019/199] Extract user agent from socket connect_info (#3815) --- lib/phoenix/endpoint.ex | 5 +++ lib/phoenix/socket/transport.ex | 29 ++++++++++++----- .../integration/websocket_channels_test.exs | 31 ++++++++++++++++++- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex index 699ae413d4..14d449aa04 100644 --- a/lib/phoenix/endpoint.ex +++ b/lib/phoenix/endpoint.ex @@ -770,8 +770,13 @@ defmodule Phoenix.Endpoint do The valid keys are: * `:peer_data` - the result of `Plug.Conn.get_peer_data/1` + * `:x_headers` - all request headers that have an "x-" prefix + * `:uri` - a `%URI{}` with information from the conn + + * `: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 diff --git a/lib/phoenix/socket/transport.ex b/lib/phoenix/socket/transport.ex index 14b8352586..45a63e2f62 100644 --- a/lib/phoenix/socket/transport.ex +++ b/lib/phoenix/socket/transport.ex @@ -248,7 +248,7 @@ defmodule Phoenix.Socket.Transport do connect_info = Enum.map(connect_info, fn - key when key in [:peer_data, :uri, :x_headers] -> + key when key in [:peer_data, :uri, :user_agent, :x_headers] -> key {:session, session} -> @@ -431,9 +431,13 @@ defmodule Phoenix.Socket.Transport do The supported keys are: * `:peer_data` - the result of `Plug.Conn.get_peer_data/1` + * `:x_headers` - a list of all request headers that have an "x-" prefix + * `:uri` - a `%URI{}` derived from the conn + * `:user_agent` - the value of the "user-agent" request header + """ def connect_info(conn, endpoint, keys) do for key <- keys, into: %{} do @@ -447,6 +451,9 @@ defmodule Phoenix.Socket.Transport do :uri -> {:uri, fetch_uri(conn)} + :user_agent -> + {:user_agent, fetch_user_agent(conn)} + {:session, session} -> {:session, connect_session(conn, endpoint, session)} @@ -519,17 +526,23 @@ defmodule Phoenix.Socket.Transport do do: pair end - defp fetch_uri(%{host: host, scheme: scheme, query_string: query_string, port: port, request_path: request_path}) do + defp fetch_uri(conn) do %URI{ - scheme: to_string(scheme), - query: query_string, - port: port, - host: host, - authority: host, - path: request_path, + scheme: to_string(conn.scheme), + query: conn.query_string, + port: conn.port, + host: conn.host, + authority: conn.host, + path: conn.request_path } end + defp fetch_user_agent(conn) do + with {_, value} <- List.keyfind(conn.req_headers, "user-agent", 0) do + value + end + end + defp check_origin_config(handler, endpoint, opts) do Phoenix.Config.cache(endpoint, {:check_origin, handler}, fn _ -> check_origin = diff --git a/test/phoenix/integration/websocket_channels_test.exs b/test/phoenix/integration/websocket_channels_test.exs index add5f9aa26..217f142f90 100644 --- a/test/phoenix/integration/websocket_channels_test.exs +++ b/test/phoenix/integration/websocket_channels_test.exs @@ -165,7 +165,14 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do websocket: [ check_origin: ["//example.com"], timeout: 200, - connect_info: [:x_headers, :peer_data, :uri, session: @session_config, signing_salt: "salt"] + connect_info: [ + :x_headers, + :peer_data, + :uri, + :user_agent, + session: @session_config, + signing_salt: "salt" + ] ] plug Plug.Session, @session_config @@ -296,6 +303,28 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do "port" => 80}}}} end + test "transport user agent is extracted to the socket connect_info" do + extra_headers = [{"user-agent", "foo/1.0"}] + + {:ok, sock} = + WebsocketClient.start_link( + self(), + "ws://127.0.0.1:#{@port}/ws/connect_info/websocket?vsn=#{@vsn}", + @serializer, + extra_headers + ) + WebsocketClient.join(sock, lobby(), %{}) + + assert_receive %Message{ + event: "joined", + payload: %{ + "connect_info" => %{ + "user_agent" => "foo/1.0" + } + } + } + end + test "transport session is extracted to the socket connect_info" do import Phoenix.ConnTest path = "ws://127.0.0.1:#{@port}/ws/connect_info/websocket?vsn=#{@vsn}" From df5f97bab88b54b76c46071bb648da036a9ef450 Mon Sep 17 00:00:00 2001 From: Mike Clark Date: Fri, 1 May 2020 14:16:50 -0600 Subject: [PATCH 020/199] Add LiveView course link to community.md (#3818) --- guides/introduction/community.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/guides/introduction/community.md b/guides/introduction/community.md index 0757cc0a3e..7c01a26983 100644 --- a/guides/introduction/community.md +++ b/guides/introduction/community.md @@ -32,6 +32,8 @@ There are a number of places to connect with community members at all experience ## Screencasts/Courses + * [Phoenix LiveView Free Course (The Pragmatic Studio - May 2020)](https://pragmaticstudio.com/courses/phoenix-liveview) + * [The Complete Elixir and Phoenix Bootcamp Master Functional Programming Techniques with Elixir and Phoenix while Learning to Build Compelling Web Applications (Udemy - Jun 2017)](https://www.udemy.com/the-complete-elixir-and-phoenix-bootcamp-and-tutorial/) * [Discover Elixir & Phoenix (Mar 2017)](https://www.ludu.co/course/discover-elixir-phoenix) From 074b1960c45844ebc8218b964bcb3a5e9811e94f Mon Sep 17 00:00:00 2001 From: Matt Date: Fri, 1 May 2020 20:19:23 +0000 Subject: [PATCH 021/199] Fixed typo: introspect (#3810) --- guides/plug.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/plug.md b/guides/plug.md index 091b668bc4..d044fc59f3 100644 --- a/guides/plug.md +++ b/guides/plug.md @@ -38,7 +38,7 @@ Pretty simple, right? Let's see this function in action by adding it to our endp defmodule HelloWeb.Endpoint do ... - plug :instrospect + plug :introspect plug HelloWeb.Router def introspect(conn, _opts) do @@ -134,7 +134,7 @@ Endpoints organize all the plugs common to every request, and apply them before defmodule HelloWeb.Endpoint do ... - plug :instrospect + plug :introspect plug HelloWeb.Router ``` From 8206d87806bfda4fb8fd3a419836746e6b05ed75 Mon Sep 17 00:00:00 2001 From: Alberto Lirussi <31094105+alirux@users.noreply.github.com> Date: Fri, 1 May 2020 22:20:15 +0200 Subject: [PATCH 022/199] Added Telemetry supervisor to Hello.Application children list in the guides/directory_structure.md (#3812) --- guides/directory_structure.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/guides/directory_structure.md b/guides/directory_structure.md index 4599e739cd..cf34fe4a6d 100644 --- a/guides/directory_structure.md +++ b/guides/directory_structure.md @@ -15,7 +15,7 @@ When we use `mix phx.new` to generates a new Phoenix application, it builds a to │ └── hello_web │ └── hello_web.ex ├── priv -├── test +└── test ``` We will go over those directories one by one: @@ -41,7 +41,7 @@ The `lib/hello` directory hosts all of your business domain. Since our project d ```console lib/hello ├── application.ex -├── repo.ex +└── repo.ex ``` The `lib/hello/application.ex` file defines an Elixir application named `Hello.Application`. That's because at the end of the day Phoenix applications are simply Elixir applications. The `Hello.Application` module defines which services are part of our application: @@ -50,6 +50,8 @@ The `lib/hello/application.ex` file defines an Elixir application named `Hello.A children = [ # Start the Ecto repository Hello.Repo, + # Start the Telemetry supervisor + HelloWeb.Telemetry, # Start the PubSub system {Phoenix.PubSub, name: Hello.PubSub}, # Start the Endpoint (http/https) @@ -90,7 +92,7 @@ lib/hello_web │ │ └── app.html.eex │ └── page │ └── index.html.eex -└── views +├── views │ ├── error_helpers.ex │ ├── error_view.ex │ ├── layout_view.ex @@ -98,6 +100,7 @@ lib/hello_web ├── endpoint.ex ├── gettext.ex ├── router.ex +└── telemetry.ex ``` All of the files which are currently in the `controllers`, `templates`, and `views` directories are there to create the "Welcome to Phoenix!" page we saw in the "Up and running" guide. @@ -105,6 +108,8 @@ The `channels` directory is where we will add code related to building real-time By looking at `templates` and `views` directories, we can see Phoenix provides features for handling layouts and error pages out of the box. -Besides the directories mentioned, `lib/hello_web` has three files at its root. `lib/hello_web/endpoint.ex` is the entry-point for HTTP requests. Once the browser accesses `http://localhost:4000`, the endpoint starts processing the data, eventually leading to the router, which is defined in `lib/hello_web/router.ex`. The router defines the rules to dispatch requests to "controllers", which then uses "views" and "templates" to render HTML pages back to clients. We explore these layers in length in other guides, starting with the "Request life-cycle" guide coming next. +Besides the directories mentioned, `lib/hello_web` has four files at its root. `lib/hello_web/endpoint.ex` is the entry-point for HTTP requests. Once the browser accesses `http://localhost:4000`, the endpoint starts processing the data, eventually leading to the router, which is defined in `lib/hello_web/router.ex`. The router defines the rules to dispatch requests to "controllers", which then uses "views" and "templates" to render HTML pages back to clients. We explore these layers in length in other guides, starting with the "Request life-cycle" guide coming next. + +Through _telemetry_, Phoenix is able to collect metrics and send monitoring events of your application. The `lib/hello_web/telemetry.ex` file defines the supervisor responsible for managing the telemetry processes. You can find more information on this topic in the [Telemetry guide](telemetry.html). Finally, there is a `lib/hello_web/gettext.ex` file which provides internationalization through [Gettext](https://hexdocs.pm/gettext/Gettext.html). If you are not worried about internationalization, you can safely skip this file and its contents. From a5cabe3e98512386a9d420a392e6db0d66b4ee0b Mon Sep 17 00:00:00 2001 From: "E. Sambo" Date: Fri, 1 May 2020 15:20:46 -0500 Subject: [PATCH 023/199] Fix grammar (#3816) --- guides/routing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/routing.md b/guides/routing.md index 2db728c840..b418489a93 100644 --- a/guides/routing.md +++ b/guides/routing.md @@ -291,7 +291,7 @@ scope "/admin", HelloWeb.Admin do end ``` -We define a new scope where all routes a prefixed with "/admin" and all controllers are under the `HelloWeb.Admin` namespace. +We define a new scope where all routes are prefixed with "/admin" and all controllers are under the `HelloWeb.Admin` namespace. Running `mix phx.routes` again, in addition to the previous set of routes we get the following: From 83b1dd27b34747fa76bc940f7ae1280f7ddabb83 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Fri, 1 May 2020 22:21:03 +0200 Subject: [PATCH 024/199] Fix docs refs (#3817) --- lib/phoenix/channel.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/phoenix/channel.ex b/lib/phoenix/channel.ex index bb9df984ee..c9dfe79851 100644 --- a/lib/phoenix/channel.ex +++ b/lib/phoenix/channel.ex @@ -339,7 +339,7 @@ defmodule Phoenix.Channel do @doc """ Handle regular Elixir process messages. - See `GenServer.handle_info/2`. + See `c:GenServer.handle_info/2`. """ @callback handle_info(msg :: term, socket :: Socket.t()) :: {:noreply, Socket.t()} @@ -348,7 +348,7 @@ defmodule Phoenix.Channel do @doc """ Handle regular GenServer call messages. - See `GenServer.handle_call/3`. + See `c:GenServer.handle_call/3`. """ @callback handle_call(msg :: term, from :: {pid, tag :: term}, socket :: Socket.t()) :: {:reply, response :: term, Socket.t()} @@ -358,7 +358,7 @@ defmodule Phoenix.Channel do @doc """ Handle regular GenServer cast messages. - See `GenServer.handle_cast/2`. + See `c:GenServer.handle_cast/2`. """ @callback handle_cast(msg :: term, socket :: Socket.t()) :: {:noreply, Socket.t()} @@ -373,7 +373,7 @@ defmodule Phoenix.Channel do @doc """ Invoked when the channel process is about to exit. - See `GenServer.terminate/2`. + See `c:GenServer.terminate/2`. """ @callback terminate( reason :: :normal | :shutdown | {:shutdown, :left | :closed | term}, From d3dc24458ced50ff63b986a32c1916064b4dc443 Mon Sep 17 00:00:00 2001 From: Aaron Renner Date: Fri, 1 May 2020 14:23:46 -0600 Subject: [PATCH 025/199] Fix formatting of generated *_live_test.exs (#3820) --- priv/templates/phx.gen.live/live_test.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/priv/templates/phx.gen.live/live_test.exs b/priv/templates/phx.gen.live/live_test.exs index 8cc5f6b780..9fccb0c48c 100644 --- a/priv/templates/phx.gen.live/live_test.exs +++ b/priv/templates/phx.gen.live/live_test.exs @@ -33,7 +33,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web {:ok, index_live, _html} = live(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) assert index_live |> element("a", "New <%= schema.human_singular %>") |> render_click() =~ - "New <%= schema.human_singular %>" + "New <%= schema.human_singular %>" assert_patch(index_live, Routes.<%= schema.route_helper %>_index_path(conn, :new)) @@ -55,7 +55,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web {:ok, index_live, _html} = live(conn, Routes.<%= schema.route_helper %>_index_path(conn, :index)) assert index_live |> element("#<%= schema.singular %>-#{<%= schema.singular %>.id} a", "Edit") |> render_click() =~ - "Edit <%= schema.human_singular %>" + "Edit <%= schema.human_singular %>" assert_patch(index_live, Routes.<%= schema.route_helper %>_index_path(conn, :edit, <%= schema.singular %>)) @@ -95,7 +95,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web {:ok, show_live, _html} = live(conn, Routes.<%= schema.route_helper %>_show_path(conn, :show, <%= schema.singular %>)) assert show_live |> element("a", "Edit") |> render_click() =~ - "Edit <%= schema.human_singular %>" + "Edit <%= schema.human_singular %>" assert_patch(show_live, Routes.<%= schema.route_helper %>_show_path(conn, :edit, <%= schema.singular %>)) From 7fb28a4eaab3deaeff9022911f7b851a3c3ab513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 2 May 2020 00:29:11 +0200 Subject: [PATCH 026/199] import assigns: 2 on channels, closes #3805 --- lib/phoenix/channel.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix/channel.ex b/lib/phoenix/channel.ex index c9dfe79851..f99ca0498d 100644 --- a/lib/phoenix/channel.ex +++ b/lib/phoenix/channel.ex @@ -402,7 +402,7 @@ defmodule Phoenix.Channel do @phoenix_shutdown Keyword.get(opts, :shutdown, 5000) import unquote(__MODULE__) - import Phoenix.Socket, only: [assign: 3] + import Phoenix.Socket, only: [assign: 3, assign: 2] def child_spec(init_arg) do %{ From e098a3e48a781efb6c1b48ab238303c8f219910a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 2 May 2020 01:25:51 +0200 Subject: [PATCH 027/199] Clarify the need for PubSub, closes #3807 --- installer/lib/mix/tasks/phx.new.ex | 15 +++++++++++++-- installer/test/phx_new_web_test.exs | 2 ++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/installer/lib/mix/tasks/phx.new.ex b/installer/lib/mix/tasks/phx.new.ex index 4ccdde4e4d..1779bef642 100644 --- a/installer/lib/mix/tasks/phx.new.ex +++ b/installer/lib/mix/tasks/phx.new.ex @@ -147,8 +147,8 @@ defmodule Mix.Tasks.Phx.New do project end - defp prompt_to_install_deps(%Project{} = project, generator, path) do - path = Map.fetch!(project, path) + defp prompt_to_install_deps(%Project{} = project, generator, path_key) do + path = Map.fetch!(project, path_key) install? = Mix.shell().yes?("\nFetch and install dependencies?") cd_step = ["$ cd #{relative_app_path(path)}"] @@ -174,6 +174,17 @@ defmodule Mix.Tasks.Phx.New do print_ecto_info(generator) end + if path_key == :web_path do + Mix.shell().info(""" + Your web app requires a PubSub server to be running. + The PubSub server is typically defined in a `mix phx.gen.ecto` app. + If you don't plan to define an Ecto app, you must explicitly start + the PubSub in your supervision tree as: + + {Phoenix.PubSub, name: #{inspect(project.app_mod)}.PubSub}} + """) + end + print_mix_info(generator) end) end diff --git a/installer/test/phx_new_web_test.exs b/installer/test/phx_new_web_test.exs index 4b828f89a2..e9c5bffb11 100644 --- a/installer/test/phx_new_web_test.exs +++ b/installer/test/phx_new_web_test.exs @@ -58,6 +58,8 @@ defmodule Mix.Tasks.Phx.New.WebTest do assert msg =~ "$ cd phx_web" assert msg =~ "$ mix deps.get" + assert_received {:mix_shell, :info, ["Your web app requires a PubSub server to be running." <> _]} + assert_received {:mix_shell, :info, ["Start your Phoenix app" <> _]} end end From e6b4a5ad0e803b0654cf33f47179cb617fb8ad75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 2 May 2020 19:14:45 +0200 Subject: [PATCH 028/199] Namespace plural assign when it conflicts with singular assign in phx.gen.live --- lib/mix/phoenix/schema.ex | 4 +++ priv/templates/phx.gen.live/index.ex | 6 ++--- priv/templates/phx.gen.live/index.html.leex | 2 +- test/mix/tasks/phx.gen.live_test.exs | 28 ++++++++++++++++++++- 4 files changed, 35 insertions(+), 5 deletions(-) diff --git a/lib/mix/phoenix/schema.ex b/lib/mix/phoenix/schema.ex index d05544e898..b63b98f03a 100644 --- a/lib/mix/phoenix/schema.ex +++ b/lib/mix/phoenix/schema.ex @@ -6,6 +6,7 @@ defmodule Mix.Phoenix.Schema do defstruct module: nil, repo: nil, table: nil, + collection: nil, embedded?: false, generate?: true, opts: [], @@ -83,6 +84,8 @@ defmodule Mix.Phoenix.Schema do |> Module.split() |> List.last() |> Phoenix.Naming.underscore() + + collection = if schema_plural == singular, do: singular <> "_collection", else: schema_plural string_attr = string_attr(types) create_params = params(attrs, :create) default_params_key = @@ -103,6 +106,7 @@ defmodule Mix.Phoenix.Schema do attrs: attrs, plural: schema_plural, singular: singular, + collection: collection, assocs: assocs, types: types, defaults: schema_defaults(attrs), diff --git a/priv/templates/phx.gen.live/index.ex b/priv/templates/phx.gen.live/index.ex index d59915c7e4..f46c53b77b 100644 --- a/priv/templates/phx.gen.live/index.ex +++ b/priv/templates/phx.gen.live/index.ex @@ -6,7 +6,7 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web @impl true def mount(_params, _session, socket) do - {:ok, assign(socket, :<%= schema.plural %>, fetch_<%= schema.plural %>())} + {:ok, assign(socket, :<%= schema.collection %>, list_<%= schema.plural %>())} end @impl true @@ -37,10 +37,10 @@ defmodule <%= inspect context.web_module %>.<%= inspect Module.concat(schema.web <%= schema.singular %> = <%= inspect context.alias %>.get_<%= schema.singular %>!(id) {:ok, _} = <%= inspect context.alias %>.delete_<%= schema.singular %>(<%= schema.singular %>) - {:noreply, assign(socket, :<%= schema.plural %>, fetch_<%=schema.plural %>())} + {:noreply, assign(socket, :<%= schema.collection %>, list_<%=schema.plural %>())} end - defp fetch_<%= schema.plural %> do + defp list_<%= schema.plural %> do <%= inspect context.alias %>.list_<%= schema.plural %>() end end diff --git a/priv/templates/phx.gen.live/index.html.leex b/priv/templates/phx.gen.live/index.html.leex index 3e9672f921..1ea9f0ab7e 100644 --- a/priv/templates/phx.gen.live/index.html.leex +++ b/priv/templates/phx.gen.live/index.html.leex @@ -18,7 +18,7 @@ - <%%= for <%= schema.singular %> <- @<%= schema.plural %> do %> + <%%= for <%= schema.singular %> <- @<%= schema.collection %> do %> <%= for {k, _} <- schema.attrs do %> <%%= <%= schema.singular %>.<%= k %> %> <% end %> diff --git a/test/mix/tasks/phx.gen.live_test.exs b/test/mix/tasks/phx.gen.live_test.exs index e08d1fdede..5afd2625b5 100644 --- a/test/mix/tasks/phx.gen.live_test.exs +++ b/test/mix/tasks/phx.gen.live_test.exs @@ -171,7 +171,7 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do end end - test "with --web namespace generates namedspaced web modules and directories", config do + test "with --web namespace generates namespaced web modules and directories", config do in_tmp_live_project config.test, fn -> Gen.Live.run(~w(Blog Post posts title:string --web Blog)) @@ -285,6 +285,32 @@ defmodule Mix.Tasks.Phx.Gen.LiveTest do end end + + test "with same singular and plural", config do + in_tmp_live_project config.test, fn -> + Gen.Live.run(~w(Tracker Series series value:integer)) + + assert_file "lib/phoenix/tracker.ex" + assert_file "lib/phoenix/tracker/series.ex" + + assert_file "lib/phoenix_web/live/series_live/index.ex", fn file -> + assert file =~ "assign(socket, :series_collection, list_series())" + end + + assert_file "lib/phoenix_web/live/series_live/show.ex" + assert_file "lib/phoenix_web/live/series_live/form_component.ex" + assert_file "lib/phoenix_web/live/modal_component.ex" + + assert_file "lib/phoenix_web/live/series_live/index.html.leex", fn file -> + assert file =~ "for series <- @series_collection do" + end + + assert_file "lib/phoenix_web/live/series_live/show.html.leex" + assert_file "lib/phoenix_web/live/series_live/form_component.html.leex" + assert_file "test/phoenix_web/live/series_live_test.exs" + end + end + describe "inside umbrella" do test "without context_app generators config uses web dir", config do in_tmp_live_umbrella_project config.test, fn -> From f771140e237f8799cc94a6709a03c217ff6b6581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 4 May 2020 00:19:27 +0200 Subject: [PATCH 029/199] Track the latest assets instead of a global hash --- installer/templates/phx_assets/app.js | 12 +----- lib/phoenix/digester.ex | 40 ++++--------------- lib/phoenix/endpoint.ex | 9 +++-- lib/phoenix/endpoint/supervisor.ex | 4 +- .../digest/cleaner/cache_manifest.json | 3 +- ...latest_not_most_recent_cache_manifest.json | 3 +- .../digest/compile/cache_manifest.json | 3 +- .../compile/cache_manifest_upgrade.json | 3 +- test/fixtures/digest/priv/static/app.js | 1 - test/phoenix/digester_test.exs | 10 ----- test/phoenix/endpoint/endpoint_test.exs | 6 ++- 11 files changed, 25 insertions(+), 69 deletions(-) diff --git a/installer/templates/phx_assets/app.js b/installer/templates/phx_assets/app.js index 1722613222..7467d54e38 100644 --- a/installer/templates/phx_assets/app.js +++ b/installer/templates/phx_assets/app.js @@ -17,16 +17,8 @@ import {Socket} from "phoenix" import NProgress from "nprogress" import {LiveSocket} from "phoenix_live_view" -// The _csrf_token is necessary to load the current session into the LiveView. -// The _cache_static_manifest_hash is used to detect whenever there is a new -// deploy and trigger a reload of the assets. The "PHOENIX_CACHE_STATIC_MANIFEST_HASH" -// will be automatically replaced by a hash by running `mix phx.digest` in prod. -let params = { - _csrf_token: document.querySelector("meta[name='csrf-token']").getAttribute("content"), - _cache_static_manifest_hash: "PHOENIX_CACHE_STATIC_MANIFEST_HASH" -} - -let liveSocket = new LiveSocket("/live", Socket, {params: params}) +let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") +let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) // Show progress bar on live navigation and form submits window.addEventListener("phx:page-loading-start", info => NProgress.start()) diff --git a/lib/phoenix/digester.ex b/lib/phoenix/digester.ex index a7ec275077..2c90453aa7 100644 --- a/lib/phoenix/digester.ex +++ b/lib/phoenix/digester.ex @@ -24,12 +24,10 @@ defmodule Phoenix.Digester do files = filter_files(input_path) latest = generate_latest(files) - hash = latest_hash(latest) - digests = load_compile_digests(output_path) - digested_files = Enum.map(files, &digested_contents(&1, latest, hash)) + digested_files = Enum.map(files, &digested_contents(&1, latest)) - save_manifest(digested_files, latest, hash, digests, output_path) + save_manifest(digested_files, latest, digests, output_path) Enum.each(digested_files, &write_to_disk(&1, output_path)) else {:error, :invalid_path} @@ -75,35 +73,27 @@ defmodule Phoenix.Digester do defp migrate_manifest(%{"version" => @manifest_version} = manifest, _output_path), do: manifest defp migrate_manifest(_latest, _output_path), do: @empty_manifest - defp save_manifest(files, latest, hash, old_digests, output_path) do + defp save_manifest(files, latest, old_digests, output_path) do old_digests_that_still_exist = old_digests |> Enum.filter(fn {file, _} -> File.exists?(Path.join(output_path, file)) end) |> Map.new() digests = Map.merge(old_digests_that_still_exist, generate_digests(files)) - write_manifest(latest, digests, hash, output_path) + write_manifest(latest, digests, output_path) end - defp write_manifest(latest, digests, hash, output_path) do + defp write_manifest(latest, digests, output_path) do json = %{ "latest" => latest, "version" => @manifest_version, - "digests" => digests, - "hash" => hash + "digests" => digests } manifest_content = Phoenix.json_library().encode!(json) File.write!(Path.join(output_path, "cache_manifest.json"), manifest_content) end - defp latest_hash(latest) do - [@manifest_version, latest] - |> :erlang.term_to_binary() - |> :erlang.md5() - |> Base.url_encode64(padding: false) - end - defp generate_digests(files) do Map.new( files, @@ -182,15 +172,9 @@ defmodule Phoenix.Digester do Path.extname(file.filename) in Application.fetch_env!(:phoenix, :gzippable_exts) end - defp digested_contents(file, latest, hash) do + defp digested_contents(file, latest) do ext = Path.extname(file.filename) - file = - case ext do - ".js" -> %{file | content: digest_javascript_contents(file, hash)} - _ -> file - end - digested_content = case ext do ".css" -> digest_stylesheet_asset_references(file, latest) @@ -202,14 +186,6 @@ defmodule Phoenix.Digester do %{file | digested_content: digested_content} end - @javascript_cache_manifest_hash_regex ~r{["'](PHOENIX_CACHE_STATIC_MANIFEST_HASH)["']} - - defp digest_javascript_contents(file, hash) do - Regex.replace(@javascript_cache_manifest_hash_regex, file.content, fn _, _ -> - "\"#{hash}\"" - end) - end - @stylesheet_url_regex ~r{(url\(\s*)(\S+?)(\s*\))} @quoted_text_regex ~r{\A(['"])(.+)\1\z} @@ -305,7 +281,7 @@ defmodule Phoenix.Digester do %{"latest" => latest, "digests" => digests} = load_manifest(path) files = files_to_clean(latest, digests, now - age, keep) remove_files(files, path) - write_manifest(latest, Map.drop(digests, files), latest_hash(latest), path) + write_manifest(latest, Map.drop(digests, files), path) :ok else {:error, :invalid_path} diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex index 14d449aa04..a96986af70 100644 --- a/lib/phoenix/endpoint.ex +++ b/lib/phoenix/endpoint.ex @@ -102,10 +102,11 @@ defmodule Phoenix.Endpoint do by `mix phx.digest`. It can be either: a string containing a file system path or a tuple containing the application name and the path within that application. - * `:cache_static_manifest_hash` - the hash of the cache static manifest. - This is automatically loaded from `cache_static_manifest` on boot. - However, if you have your own static handling mechanism, you may want to - set this value explicitly. + * `:cache_static_manifest_latest` - a map of the static files pointing to their + digest version. This is automatically loaded from `cache_static_manifest` on + boot. However, if you have your own static handling mechanism, you may want to + set this value explicitly. This is used by projects such as `LiveView` to + detect if the client is running on the latest version of all assets * `:check_origin` - configure the default `:check_origin` setting for transports. See `socket/3` for options. Defaults to `true`. diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex index 69801ff9d4..bba344ffdc 100644 --- a/lib/phoenix/endpoint/supervisor.ex +++ b/lib/phoenix/endpoint/supervisor.ex @@ -431,8 +431,8 @@ defmodule Phoenix.Endpoint.Supervisor do endpoint.static_path("/") end - defp warmup_static(endpoint, %{"latest" => latest, "digests" => digests} = manifest) do - Phoenix.Config.put_new(endpoint, :cache_static_manifest_hash, manifest["hash"]) + defp warmup_static(endpoint, %{"latest" => latest, "digests" => digests}) do + Phoenix.Config.put_new(endpoint, :cache_static_manifest_latest, latest) Enum.each(latest, fn {key, _} -> Phoenix.Config.cache(endpoint, {:__phoenix_static__, "/" <> key}, fn _ -> diff --git a/test/fixtures/digest/cleaner/cache_manifest.json b/test/fixtures/digest/cleaner/cache_manifest.json index 68155e0cba..3c3c7b7b63 100644 --- a/test/fixtures/digest/cleaner/cache_manifest.json +++ b/test/fixtures/digest/cleaner/cache_manifest.json @@ -22,6 +22,5 @@ "latest": { "app.css": "app-3.css" }, - "version": 1, - "hash": "AxLYL9wnimiMdf3_NDnYvw" + "version": 1 } diff --git a/test/fixtures/digest/cleaner/latest_not_most_recent_cache_manifest.json b/test/fixtures/digest/cleaner/latest_not_most_recent_cache_manifest.json index 60e7c08d90..78ace07d6c 100644 --- a/test/fixtures/digest/cleaner/latest_not_most_recent_cache_manifest.json +++ b/test/fixtures/digest/cleaner/latest_not_most_recent_cache_manifest.json @@ -22,6 +22,5 @@ "latest": { "app.css": "app-3.css" }, - "version": 1, - "hash": "AxLYL9wnimiMdf3_NDnYvw" + "version": 1 } diff --git a/test/fixtures/digest/compile/cache_manifest.json b/test/fixtures/digest/compile/cache_manifest.json index 140d1f4870..dc286f9c6e 100644 --- a/test/fixtures/digest/compile/cache_manifest.json +++ b/test/fixtures/digest/compile/cache_manifest.json @@ -10,6 +10,5 @@ "latest": { "foo.css": "foo-d978852bea6530fcd197b5445ed008fd.css" }, - "version": 1, - "hash": "cjkuB6uDZecddGSFz7D7kg" + "version": 1 } diff --git a/test/fixtures/digest/compile/cache_manifest_upgrade.json b/test/fixtures/digest/compile/cache_manifest_upgrade.json index 0a32d87280..23bce0f2f3 100644 --- a/test/fixtures/digest/compile/cache_manifest_upgrade.json +++ b/test/fixtures/digest/compile/cache_manifest_upgrade.json @@ -16,6 +16,5 @@ "latest": { "foo.css": "foo-ghijkl.css" }, - "version": 1, - "hash": "3xAzpSnmcgte3bMevWqJMA" + "version": 1 } diff --git a/test/fixtures/digest/priv/static/app.js b/test/fixtures/digest/priv/static/app.js index 0fa8d13429..172e3a596b 100644 --- a/test/fixtures/digest/priv/static/app.js +++ b/test/fixtures/digest/priv/static/app.js @@ -1,5 +1,4 @@ (function() { - var digest = "PHOENIX_CACHE_STATIC_MANIFEST_HASH"; console.log('Hello World!'); })(); //# sourceMappingURL=app.js.map diff --git a/test/phoenix/digester_test.exs b/test/phoenix/digester_test.exs index 6eef0f5d05..03b25c2ab0 100644 --- a/test/phoenix/digester_test.exs +++ b/test/phoenix/digester_test.exs @@ -35,7 +35,6 @@ defmodule Phoenix.DigesterTest do json = Path.join(@output_path, "cache_manifest.json") |> json_read!() assert json["latest"]["phoenix.png"] =~ ~r"phoenix-#{@hash_regex}.png" assert json["version"] == 1 - assert byte_size(json["hash"]) == 22 end test "includes existing digests in new cache manifest" do @@ -131,7 +130,6 @@ defmodule Phoenix.DigesterTest do json = Path.join(input_path, "cache_manifest.json") |> json_read!() assert json["digests"] == %{} - assert byte_size(json["hash"]) == 22 end test "digests and compress nested files" do @@ -169,8 +167,6 @@ defmodule Phoenix.DigesterTest do json2 = Path.join(@output_path, "cache_manifest.json") |> json_read!() assert Enum.count(json2["digests"]) == 2 - - assert json1["hash"] != json2["hash"] end test "doesn't duplicate files when digesting and compressing twice" do @@ -182,11 +178,8 @@ defmodule Phoenix.DigesterTest do File.write!(input_file, "console.log('test');") assert :ok = Phoenix.Digester.compile(input_path, input_path) - json1 = Path.join(input_path, "cache_manifest.json") |> json_read!() assert :ok = Phoenix.Digester.compile(input_path, input_path) - json2 = Path.join(input_path, "cache_manifest.json") |> json_read!() - assert json1["hash"] == json2["hash"] output_files = assets_files(input_path) refute "file.js.gz.gz" in output_files refute "cache_manifest.json.gz" in output_files @@ -239,7 +232,6 @@ defmodule Phoenix.DigesterTest do test "digests sourceMappingURL asset paths found within javascript source files" do input_path = "test/fixtures/digest/priv/static/" assert :ok = Phoenix.Digester.compile(input_path, @output_path) - json = Path.join(@output_path, "cache_manifest.json") |> json_read!() digested_js_map_filename = assets_files(@output_path) @@ -252,12 +244,10 @@ defmodule Phoenix.DigesterTest do digested_js = Path.join(@output_path, digested_js_filename) |> File.read!() refute digested_js =~ "app.js.map" assert digested_js =~ "#{digested_js_map_filename}" - assert digested_js =~ "var digest = \"#{json["hash"]}\";" js = Path.join(@output_path, "app.js") |> File.read!() assert js =~ "app.js.map" refute js =~ "#{digested_js_map_filename}" - assert js =~ "var digest = \"#{json["hash"]}\";" end test "digests file url paths found within javascript mapping files" do diff --git a/test/phoenix/endpoint/endpoint_test.exs b/test/phoenix/endpoint/endpoint_test.exs index 79fe3c0fd8..78de1fdf39 100644 --- a/test/phoenix/endpoint/endpoint_test.exs +++ b/test/phoenix/endpoint/endpoint_test.exs @@ -95,14 +95,16 @@ defmodule Phoenix.Endpoint.EndpointTest do end test "warms up caches on load and config change" do - assert Endpoint.config(:cache_static_manifest_hash) == "cjkuB6uDZecddGSFz7D7kg" + assert Endpoint.config(:cache_static_manifest_latest) == + %{"foo.css" => "foo-d978852bea6530fcd197b5445ed008fd.css"} + assert Endpoint.static_path("/foo.css") == "/foo-d978852bea6530fcd197b5445ed008fd.css?vsn=d" # Trigger a config change and the cache should be warmed up again config = put_in(@config[:cache_static_manifest], "../../../../test/fixtures/digest/compile/cache_manifest_upgrade.json") assert Endpoint.config_change([{Endpoint, config}], []) == :ok - assert Endpoint.config(:cache_static_manifest_hash) == "3xAzpSnmcgte3bMevWqJMA" + assert Endpoint.config(:cache_static_manifest_latest) == %{"foo.css" => "foo-ghijkl.css"} assert Endpoint.static_path("/foo.css") == "/foo-ghijkl.css?vsn=d" end From febb0d0d01d9bda3a6374cee7e06bb52c1ea4f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 4 May 2020 12:59:50 +0200 Subject: [PATCH 030/199] Undeprecate subscribe and unsubscribe from endpoint --- lib/phoenix/endpoint.ex | 4 +--- test/phoenix/endpoint/endpoint_test.exs | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex index a96986af70..2d79466d99 100644 --- a/lib/phoenix/endpoint.ex +++ b/lib/phoenix/endpoint.ex @@ -379,12 +379,10 @@ defmodule Phoenix.Endpoint do defp pubsub() do quote do - @deprecated "#{inspect(__MODULE__)}.subscribe/2 is deprecated, please call Phoenix.PubSub directly instead" def subscribe(topic, opts \\ []) when is_binary(topic) do - Phoenix.PubSub.subscribe(pubsub_server!(), topic, []) + Phoenix.PubSub.subscribe(pubsub_server!(), topic, opts) end - @deprecated "#{inspect(__MODULE__)}.unsubscribe/1 is deprecated, please call Phoenix.PubSub directly instead" def unsubscribe(topic) do Phoenix.PubSub.unsubscribe(pubsub_server!(), topic) end diff --git a/test/phoenix/endpoint/endpoint_test.exs b/test/phoenix/endpoint/endpoint_test.exs index 78de1fdf39..26d3846854 100644 --- a/test/phoenix/endpoint/endpoint_test.exs +++ b/test/phoenix/endpoint/endpoint_test.exs @@ -146,7 +146,7 @@ defmodule Phoenix.Endpoint.EndpointTest do end test "injects pubsub broadcast with configured server" do - Phoenix.PubSub.subscribe(:endpoint_pub, "sometopic") + Endpoint.subscribe("sometopic") some = spawn fn -> :ok end Endpoint.broadcast_from(some, "sometopic", "event1", %{key: :val}) From 6b5c28bae56cf0b8a3d049a256a1597f5ef397a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 4 May 2020 13:00:24 +0200 Subject: [PATCH 031/199] Update CHANGELOG --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d1ec92c80..7e70a44bb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,6 @@ Phoenix built-in guides have been restructured and revamped, providing a better * [ChannelTest] `use Phoenix.ChannelTest` is deprecated in favor of `import Phoenix.ChannelTest` * [ConnTest] `use Phoenix.ConnTest` is deprecated in favor of `import Plug.Conn; import Phoenix.ConnTest` * [Endpoint] The outdated `Phoenix.Endpoint.CowboyAdapter` for Cowboy 1 is deprecated. Please make sure `{:plug_cowboy, "~> 2.1"}` or later is listed in your `mix.exs` - * [Endpoint] `subscribe` and `unsubscribe` via the endpoint is deprecated, please use `Phoenix.PubSub` directly instead * [Endpoint] `Phoenix.Endpoint.instrument/4` is deprecated and has no effect. Use `:telemetry` instead. See `Phoenix.Logger` for more information * [Endpoint] The `:pubsub` key for endpoint is deprecated. Once you start your app, you will see step-by-step instructions on how to use the new PubSub config * [Layout] Use `<%= @inner_content %>` instead of `<%= render @view_module, @view_template, assigns %>` for rendering the child layout From 0c89797abcfae90099c20e52d042af891e369e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 4 May 2020 13:10:27 +0200 Subject: [PATCH 032/199] Update CHANGELOG --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e70a44bb2..1e54ce7766 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,16 @@ Then once you start an application, you will get a warning about the `:pubsub` k Phoenix built-in guides have been restructured and revamped, providing a better navigation structure and more content. +## 1.5.2-dev + +### Enhancements + * [Channel] import `assigns:` 2 on channels + * [Endpoint] Track latest static in `config(:cache_static_manifest_latest)` + * [Endpoint] Allow `:user_agent` on `connect_info` + +### Bug fixes + * [Endpoint] Undeprecate `subscribe` and `unsubscribe` in the endpoint + ## 1.5.1 (2020-04-23) ### Bug Fixes From 2247d4dbdbc3823c5179fc1bd6c2dcc7b8aece1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 4 May 2020 14:44:42 +0200 Subject: [PATCH 033/199] Add phx-track-static to root assets --- installer/templates/phx_live/templates/layout/root.html.leex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/installer/templates/phx_live/templates/layout/root.html.leex b/installer/templates/phx_live/templates/layout/root.html.leex index adad0063ec..ebef2187de 100644 --- a/installer/templates/phx_live/templates/layout/root.html.leex +++ b/installer/templates/phx_live/templates/layout/root.html.leex @@ -6,8 +6,8 @@ <%%= csrf_meta_tag() %> <%%= live_title_tag assigns[:page_title] || "<%= app_module %>", suffix: " · Phoenix Framework" %> - "/> - + "/> +
From 11e63ad5ed5eab098504907a234cf7b80ff84322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 5 May 2020 00:20:44 +0200 Subject: [PATCH 034/199] Add a quick description to telemetry_poller --- installer/templates/phx_web/telemetry.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/installer/templates/phx_web/telemetry.ex b/installer/templates/phx_web/telemetry.ex index ec20cd3bf7..ab776d989f 100644 --- a/installer/templates/phx_web/telemetry.ex +++ b/installer/templates/phx_web/telemetry.ex @@ -9,6 +9,8 @@ defmodule <%= web_namespace %>.Telemetry do @impl true def init(_arg) do children = [ + # Telemetry poller will execute the given period measurements + # every 10_000ms. Learn more here: https://hexdocs.pm/telemetry_metrics {:telemetry_poller, measurements: periodic_measurements(), period: 10_000} # Add reporters as children of your supervision tree. # {Telemetry.Metrics.ConsoleReporter, metrics: metrics()} From af83d881cb480b4b4d3f5bac28632756e9048621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 9 May 2020 13:30:47 +0200 Subject: [PATCH 035/199] Do not copy messages through supervisor process --- lib/phoenix/channel/server.ex | 16 ++++++++++------ lib/phoenix/socket.ex | 14 +++++++------- .../integration/websocket_channels_test.exs | 14 +++++++++----- 3 files changed, 26 insertions(+), 18 deletions(-) diff --git a/lib/phoenix/channel/server.ex b/lib/phoenix/channel/server.ex index cd402e2343..a608f4b68d 100644 --- a/lib/phoenix/channel/server.ex +++ b/lib/phoenix/channel/server.ex @@ -22,10 +22,11 @@ defmodule Phoenix.Channel.Server do ref = make_ref() from = {self(), ref} - child_spec = channel.child_spec({payload, from, socket}) + child_spec = channel.child_spec({socket.endpoint, from}) case PoolSupervisor.start_child(socket.endpoint, socket.handler, from, child_spec) do {:ok, pid} -> + send(pid, {Phoenix.Channel, payload, from, socket}) mon_ref = Process.monitor(pid) receive do @@ -254,10 +255,8 @@ defmodule Phoenix.Channel.Server do ## Callbacks @doc false - def init({auth_payload, from, socket}) do - # TODO: Use handle_continue when we support Erlang/OTP 21+. - send(self(), {:join, __MODULE__}) - {:ok, {auth_payload, from, socket}} + def init({_endpoint, {pid, _}}) do + {:ok, Process.monitor(pid)} end @doc false @@ -285,7 +284,8 @@ defmodule Phoenix.Channel.Server do end @doc false - def handle_info({:join, __MODULE__}, {auth_payload, {pid, _} = from, socket}) do + def handle_info({Phoenix.Channel, auth_payload, {pid, _} = from, socket}, ref) do + Process.demonitor(ref) %{channel: channel, topic: topic, private: private} = socket Process.put(:"$callers", [pid]) @@ -329,6 +329,10 @@ defmodule Phoenix.Channel.Server do |> handle_result(:handle_out) end + def handle_info({:DOWN, ref, _, _, reason}, ref) do + {:stop, reason, ref} + end + def handle_info({:DOWN, _, _, transport_pid, reason}, %{transport_pid: transport_pid} = socket) do reason = if reason == :normal, do: {:shutdown, :closed}, else: reason {:stop, reason, socket} diff --git a/lib/phoenix/socket.ex b/lib/phoenix/socket.ex index a5fff3d6aa..74a0be896b 100644 --- a/lib/phoenix/socket.ex +++ b/lib/phoenix/socket.ex @@ -129,9 +129,14 @@ defmodule Phoenix.Socket do ## Custom channels You can list any module as a channel as long as it implements - a `start_link/1` function that receives a tuple with three elements: + a `child_spec/1` function. The `child_spec/1` function receives + the caller as argument and it must return a child spec that + initializes a process. - {auth_payload, from, socket} + Once the process is initialized, it will receive the following + message: + + {Phoenix.Channel, auth_payload, from, socket} A custom channel implementation MUST invoke `GenServer.reply(from, {:ok | :error, reply_payload})` during its @@ -174,11 +179,6 @@ defmodule Phoenix.Socket do Custom channel implementations cannot be tested with `Phoenix.ChannelTest` and are currently considered experimental. The underlying API may be changed at any moment. - - **Note:** in future Phoenix versions we will require custom channels - to provide a custom `child_spec/1` function instead of `start_link/1`. - Since the default behaviour of `child_spec/1` is to invoke `start_link/1`, - this behaviour should be backwards compatible in almost all cases. """ require Logger diff --git a/test/phoenix/integration/websocket_channels_test.exs b/test/phoenix/integration/websocket_channels_test.exs index 217f142f90..fdc0699a4f 100644 --- a/test/phoenix/integration/websocket_channels_test.exs +++ b/test/phoenix/integration/websocket_channels_test.exs @@ -64,20 +64,24 @@ defmodule Phoenix.Integration.WebSocketChannelsTest do defmodule CustomChannel do use GenServer, restart: :temporary - def start_link(triplet) do - GenServer.start_link(__MODULE__, triplet) + def start_link(from) do + GenServer.start_link(__MODULE__, from) end - def init({payload, from, socket}) do + def init({_, _}) do + {:ok, :init} + end + + def handle_info({Phoenix.Channel, payload, from, socket}, :init) do case payload["action"] do "ok" -> GenServer.reply(from, {:ok, %{"action" => "ok"}}) - {:ok, socket} + {:noreply, socket} "ignore" -> GenServer.reply(from, {:error, %{"action" => "ignore"}}) send(self(), :stop) - {:ok, socket} + {:noreply, socket} "error" -> raise "oops" From a4bbbf570a61d76178b30fdb1e4171809caba294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 13 May 2020 10:08:23 +0200 Subject: [PATCH 036/199] Add Windows note whenever we see --prefix --- guides/deployment/deployment.md | 2 ++ guides/deployment/releases.md | 2 ++ lib/phoenix/endpoint/watcher.ex | 4 ++-- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/guides/deployment/deployment.md b/guides/deployment/deployment.md index 3834acb1c4..fe1d403794 100644 --- a/guides/deployment/deployment.md +++ b/guides/deployment/deployment.md @@ -52,6 +52,8 @@ $ mix phx.digest Check your digested files at "priv/static". ``` +*Note:* the `--prefix` flag on `npm` may not work on Windows. If so, replace the first command by `cd assets && npm run deply && cd ..`. + And that is it! The first command builds the assets and the second generates digests as well as a cache manifest file so Phoenix can quickly serve assets in production. Keep in mind that, if you by any chance forget to run the steps above, Phoenix will show an error message: diff --git a/guides/deployment/releases.md b/guides/deployment/releases.md index dfaef3bf8b..1f4fed47cd 100644 --- a/guides/deployment/releases.md +++ b/guides/deployment/releases.md @@ -45,6 +45,8 @@ $ npm run deploy --prefix ./assets $ mix phx.digest ``` +*Note:* the `--prefix` flag on `npm` may not work on Windows. If so, replace the first command by `cd assets && npm run deply && cd ..`. + And now run `mix release`: ```console diff --git a/lib/phoenix/endpoint/watcher.ex b/lib/phoenix/endpoint/watcher.ex index 928f07ea20..e6c2de7e10 100644 --- a/lib/phoenix/endpoint/watcher.ex +++ b/lib/phoenix/endpoint/watcher.ex @@ -47,13 +47,13 @@ defmodule Phoenix.Endpoint.Watcher do !System.find_executable("node") -> Logger.error "Could not start watcher because \"node\" is not available. Your Phoenix " <> "application is still running, however assets won't be compiled. " <> - "You may fix this by installing \"node\" and then running \"npm install --prefix assets\"." + "You may fix this by installing \"node\" and then running \"npm install\" inside the \"assets\" directory." exit(:shutdown) not File.exists?(script_path) -> Logger.error "Could not start node watcher because script #{inspect script_path} does not " <> "exist. Your Phoenix application is still running, however assets " <> - "won't be compiled. You may fix this by running \"npm install --prefix assets\"." + "won't be compiled. You may fix this by running \"npm install\" inside the \"assets\" directory." exit(:shutdown) true -> :ok From f90618956c7dffa426ae6bb2fed3f58a9f7849ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 12 May 2020 10:11:55 +0200 Subject: [PATCH 037/199] Do not refer to mix setup due to Windows --- installer/templates/phx_single/README.md | 4 +++- installer/templates/phx_umbrella/apps/app_name_web/README.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/installer/templates/phx_single/README.md b/installer/templates/phx_single/README.md index f3b0f82837..dc79271bd3 100644 --- a/installer/templates/phx_single/README.md +++ b/installer/templates/phx_single/README.md @@ -2,7 +2,9 @@ To start your Phoenix server: - * Setup the project with `mix setup` + * Install dependencies with `mix deps.get`<%= if ecto do %> + * Create and migrate your database with `mix ecto.setup`<% end %><%= if webpack do %> + * Install Node.js dependencies with `npm install` inside the `assets` directory<% end %> * Start Phoenix endpoint with `mix phx.server` Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. diff --git a/installer/templates/phx_umbrella/apps/app_name_web/README.md b/installer/templates/phx_umbrella/apps/app_name_web/README.md index d7a097f845..8a7038673d 100644 --- a/installer/templates/phx_umbrella/apps/app_name_web/README.md +++ b/installer/templates/phx_umbrella/apps/app_name_web/README.md @@ -2,7 +2,9 @@ To start your Phoenix server: - * Setup the project with `mix setup` + * Install dependencies with `mix deps.get`<%= if ecto do %> + * Create and migrate your database with `mix ecto.setup`<% end %><%= if webpack do %> + * Install Node.js dependencies with `npm install` inside the `assets` directory<% end %> * Start Phoenix endpoint with `mix phx.server` Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. From 017ce77f8a21fd28b64370eb4edde171bd4f8ab5 Mon Sep 17 00:00:00 2001 From: sebashwa Date: Mon, 11 May 2020 21:28:51 +0200 Subject: [PATCH 038/199] Fix variable name in code example for presence doc (#3831) --- lib/phoenix/presence.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix/presence.ex b/lib/phoenix/presence.ex index bac30014a1..155499d2ce 100644 --- a/lib/phoenix/presence.ex +++ b/lib/phoenix/presence.ex @@ -97,7 +97,7 @@ defmodule Phoenix.Presence do information to include any additional information. For example: def fetch(_topic, presences) do - users = entries |> Map.keys() |> Accounts.get_users_map() + users = presences |> Map.keys() |> Accounts.get_users_map() for {key, %{metas: metas}} <- presences, into: %{} do {key, %{metas: metas, user: users[key]}} From 69344c0e2c95195f3f31386843ae24e0897d6c5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Britto?= Date: Mon, 11 May 2020 16:28:28 -0300 Subject: [PATCH 039/199] Fix Cowboy.Drainer link (#3830) --- lib/phoenix/endpoint/cowboy2_adapter.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix/endpoint/cowboy2_adapter.ex b/lib/phoenix/endpoint/cowboy2_adapter.ex index 7047dcc0e0..3ad326dfa3 100644 --- a/lib/phoenix/endpoint/cowboy2_adapter.ex +++ b/lib/phoenix/endpoint/cowboy2_adapter.ex @@ -16,7 +16,7 @@ defmodule Phoenix.Endpoint.Cowboy2Adapter do * `:drainer` - a drainer process that triggers when your application is shutting to wait for any on-going request to finish. It accepts all - options as defined by [`Plug.Cowboy`](https://hexdocs.pm/plug_cowboy/Plug.Cowboy.Drainer.httml). + options as defined by [`Plug.Cowboy`](https://hexdocs.pm/plug_cowboy/Plug.Cowboy.Drainer.html). Defaults to `[]` and can be disabled by setting it to false. ## Custom dispatch options From ea29c6581a5a16ca7f14f0e6bda39132aefe0c84 Mon Sep 17 00:00:00 2001 From: Sophie DeBenedetto Date: Mon, 11 May 2020 15:26:16 -0400 Subject: [PATCH 040/199] Some suggested updates for clarity/context (#3814) --- guides/telemetry.md | 57 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/guides/telemetry.md b/guides/telemetry.md index 4386868d17..1c11b92361 100644 --- a/guides/telemetry.md +++ b/guides/telemetry.md @@ -13,6 +13,14 @@ and we will guide you through the steps to properly instrument your own functions using `:telemetry`. Let's take a closer look at how Telemetry works in your application. +## Overview + +The `[:telemetry]` library allows you to emit events at various stages of an application's lifecycle. You can then respond to these events by, among other things, aggregating them as metrics and sending the metrics data to a reporting destination. + +Telemetry stores events by their name in an ETS table, along with the handler for each event. Then, when a given event is executed, Telemetry looks up its handler and invokes it. + +Phoenix's Telemetry tooling provides you with a supervisor that uses `Telemetry.Metrics` to define the list of Telemetry events to handle and how to handle those events, i.e. how to structure them as a certain type of metric. This supervisor works together with Telemetry reporters to respond to the specified Telemetry events by aggregating them as the appropriate metric and sending them to the correct reporting destination. + ## The Telemetry supervisor Since v1.5, new Phoenix applications are generated with a @@ -103,7 +111,7 @@ children = [ ## Telemetry Events Many Elixir libraries (including Phoenix) are already using -the `:telemetry` package (http://hexdocs.pm/telemetry) as a +the [`:telemetry`](http://hexdocs.pm/telemetry) package as a way to give users more insight into the behavior of their applications, by emitting events at key moments in the application lifecycle. @@ -124,7 +132,7 @@ A Telemetry event is made up of the following: Here is an example of an event from your endpoint: * `[:phoenix, :endpoint, :stop]` - dispatched by - `Plug.Telemetry` in your endpoint whenever the response is + `Plug.Telemetry`, one of the default plugs in your endpoint, whenever the response is sent * Measurement: `%{duration: native_time}` @@ -148,11 +156,15 @@ took to get the response: > ― `Telemetry.Metrics` The Telemetry.Metrics package provides a common interface -for defining metrics. What this means is that the package -does not perform any aggregation of the measurements itself, -but instead relies on reporters to do so. We will discuss +for defining metrics. It exposes a set of [five metric type functions](https://hexdocs.pm/telemetry_metrics/Telemetry.Metrics.html#module-metrics) that are responsible for structuring a given Telemetry event as a particular measurement. + +The package does not perform any aggregation of the measurements itself. Instead, it provides a reporter with the Telemetry event-as-measurement definition and the reporter uses that definition to perform aggregations and report them. + +We will discuss reporters in the next section. +Let's take a look at some examples. + Using `Telemetry.Metrics`, you can define a counter metric, which counts how many HTTP requests were completed: @@ -182,11 +194,30 @@ documentation. ### An Ecto Example -Like Phoenix, Ecto ships with built-in telemetry events. +Like Phoenix, Ecto ships with built-in Telemetry events. This means that you can gain introspection into your web and database layers using the same tools. -For instance, you might want to graph query execution time: +Here is an example of a Telemetry event executed by Ecto when an Ecto repository starts: + +* `[:ecto, :repo, :init]` - dispatched by + `Ecto.Repo.Supervisor` + + * Measurement: `%{system_time: native_time}` + + * Metadata: `%{repo: Ecto.Repo, opts: Keyword.t()}` + +This means that whenever the `Ecto.Repo.Supervisor` starts, it will emit an event, via `:telemetry`, +with a measurement of the time at start-up. + +```elixir +:telemetry.execute([:ecto, :repo, :init], %{system_time: System.system_time()}, %{repo: repo, opts: opts}) +``` + +Additional Telemetry events are executed by Ecto adapters. + +One such adapter-specific event is the `[:my_app, :repo, :query]` event. +For instance, if you want to graph query execution time, you can use the `Telemetry.Metrics.summary/2` function to instruct your reporter to calculate statistics of the `[:my_app, :repo, :query]` event, like maximum, mean, percentiles etc.: ```elixir Telemetry.Metrics.summary("my_app.repo.query.query_time", @@ -194,7 +225,7 @@ Telemetry.Metrics.summary("my_app.repo.query.query_time", ) ``` -or view how long queries spend queued: +Or you could use the `Telemetry.Metrics.distribution/2` function to define a histogram for another adapter-specific event: `[:my_app, :repo, :query, :queue_time]`, thus visualizing how long queries spend queued: ```elixir Telemetry.Metrics.distribution("my_app.repo.query.queue_time", @@ -220,6 +251,16 @@ interface provided by `Telemetry.Metrics`. They then aggregate the measurements (data) into metrics to provide meaningful information about your application. +For example, if the following `Telemetry.Metrics.summary/2` call is added to the `metrics/0` function of your Telemetry supervisor: + +```elixir +summary("phoenix.endpoint.stop.duration", + unit: {:native, :millisecond} +) +``` + +Then the reporter will attach a listener for the `"phoenix.endpoint.stop.duration"` event and will respond to this event by calculating a summary metric with the given event metadata and reporting on that metric to the appropriate source. + ### Phoenix.LiveDashboard For developers interested in real-time visualizations for From e76eab4f62b7e49cb8920eb5b0aa747b1cdcdc3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 11 May 2020 16:17:57 +0200 Subject: [PATCH 041/199] Add comments to fallback controller, closes #3835 --- priv/templates/phx.gen.json/fallback_controller.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/priv/templates/phx.gen.json/fallback_controller.ex b/priv/templates/phx.gen.json/fallback_controller.ex index a4b88bf7aa..6ae62aa422 100644 --- a/priv/templates/phx.gen.json/fallback_controller.ex +++ b/priv/templates/phx.gen.json/fallback_controller.ex @@ -6,14 +6,16 @@ defmodule <%= inspect context.web_module %>.FallbackController do """ use <%= inspect context.web_module %>, :controller - <%= if schema.generate? do %>def call(conn, {:error, %Ecto.Changeset{} = changeset}) do + <%= if schema.generate? do %># This clause handles errors returned by Ecto's insert/update/delete. + def call(conn, {:error, %Ecto.Changeset{} = changeset}) do conn |> put_status(:unprocessable_entity) |> put_view(<%= inspect context.web_module %>.ChangesetView) |> render("error.json", changeset: changeset) end - <% end %>def call(conn, {:error, :not_found}) do + <% end %># This clause is an example of how to handle resources that cannot be found. + def call(conn, {:error, :not_found}) do conn |> put_status(:not_found) |> put_view(<%= inspect context.web_module %>.ErrorView) From 79612ab6158f24052203f0945a648a3eaca18003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=E1=BA=A9m=20Hu=E1=BB=B3nh?= Date: Mon, 11 May 2020 15:24:31 +0200 Subject: [PATCH 042/199] Update Phoenix.Logger docs (#3834) * Update logger.ex * Update logger.ex --- lib/phoenix/logger.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix/logger.ex b/lib/phoenix/logger.ex index 2d859d805b..3460a55b1d 100644 --- a/lib/phoenix/logger.ex +++ b/lib/phoenix/logger.ex @@ -69,7 +69,7 @@ defmodule Phoenix.Logger do * `[:phoenix, :channel_joined]` - dispatched at the end of a channel join * Measurement: `%{duration: native_time}` - * Metadata: `%{params: term, socket: Phoenix.Socket.t}` + * Metadata: `%{result: :ok | :error, params: term, socket: Phoenix.Socket.t}` * Disable logging: This event cannot be disabled * `[:phoenix, :channel_handled_in]` - dispatched at the end of a channel handle in From 54bf61c2fd62b3eebc5cf91b138a6e8686b56bbb Mon Sep 17 00:00:00 2001 From: Stephen Bussey Date: Wed, 6 May 2020 15:22:22 -0400 Subject: [PATCH 043/199] Fix Cowboy.Drainer link (#3823) --- lib/phoenix/endpoint.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex index 2d79466d99..b32315bd64 100644 --- a/lib/phoenix/endpoint.ex +++ b/lib/phoenix/endpoint.ex @@ -202,7 +202,7 @@ defmodule Phoenix.Endpoint do * `:drainer` - a drainer process that triggers when your application is shutting to wait for any on-going request to finish. It accepts all - options as defined by [`Plug.Cowboy`](https://hexdocs.pm/plug_cowboy/Plug.Cowboy.Drainer.httml). + options as defined by [`Plug.Cowboy`](https://hexdocs.pm/plug_cowboy/Plug.Cowboy.Drainer.html). Defaults to `[]` and can be disabled by setting it to false. ## Endpoint API From 7df7f641f5967bf46b9fad77cd212d87ad42f591 Mon Sep 17 00:00:00 2001 From: Dmitry Kontsevoy Date: Tue, 5 May 2020 19:21:59 +0300 Subject: [PATCH 044/199] fix bufferedAmount check in js --- assets/js/phoenix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/phoenix.js b/assets/js/phoenix.js index 2ca1db3c10..0c2e579ad0 100644 --- a/assets/js/phoenix.js +++ b/assets/js/phoenix.js @@ -955,7 +955,7 @@ export class Socket { } waitForBufferDone(callback, tries = 1) { - if (tries === 5 || !this.conn || (this.conn.bufferedAmount && this.conn.bufferedAmount === 0)) { + if (tries === 5 || !this.conn || !this.conn.bufferedAmount) { callback() return } From 61416af65cab9b5421eacf42cc3cea288dad8cee Mon Sep 17 00:00:00 2001 From: Tan Jay Jun Date: Sat, 16 May 2020 19:24:52 +0800 Subject: [PATCH 045/199] Improve CHANGELOG.md (#3849) --- CHANGELOG.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e54ce7766..7c0c80424a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ See the [upgrade guides](https://gist.github.com/chrismccord/e53e79ef8b34adf5d81 ## Phoenix.PubSub 2.0 released -Phoenix.PubSub 2.0 has been released with a more flexible and powerful fastlane mechanism. We use this opportunity to also move Phoenix.PubSub out of the endpoint and explicitly into your supervision tree. To update, you will need to remove or update the `{:phoenix_pubsub, "~> 1.x"}` entry in your `mix.exs` to at least "2.0". +Phoenix.PubSub 2.0 has been released with a more flexible and powerful fastlane mechanism. We use this opportunity to also move Phoenix.PubSub out of the endpoint and explicitly into your supervision tree. To update, you will need to remove or update the `{:phoenix_pubsub, "~> 1.x"}` entry in your `mix.exs` to at least `2.0`. Then once you start an application, you will get a warning about the `:pubsub` key in your endpoint being deprecated. Follow the steps in the warning and you are good to go! @@ -15,7 +15,7 @@ Phoenix built-in guides have been restructured and revamped, providing a better ## 1.5.2-dev ### Enhancements - * [Channel] import `assigns:` 2 on channels + * [Channel] Import `assigns: 2` on channels * [Endpoint] Track latest static in `config(:cache_static_manifest_latest)` * [Endpoint] Allow `:user_agent` on `connect_info` @@ -46,9 +46,9 @@ Phoenix built-in guides have been restructured and revamped, providing a better * [ChannelTest] `use Phoenix.ChannelTest` is deprecated in favor of `import Phoenix.ChannelTest` * [ConnTest] `use Phoenix.ConnTest` is deprecated in favor of `import Plug.Conn; import Phoenix.ConnTest` - * [Endpoint] The outdated `Phoenix.Endpoint.CowboyAdapter` for Cowboy 1 is deprecated. Please make sure `{:plug_cowboy, "~> 2.1"}` or later is listed in your `mix.exs` - * [Endpoint] `Phoenix.Endpoint.instrument/4` is deprecated and has no effect. Use `:telemetry` instead. See `Phoenix.Logger` for more information - * [Endpoint] The `:pubsub` key for endpoint is deprecated. Once you start your app, you will see step-by-step instructions on how to use the new PubSub config + * [Endpoint] The outdated `Phoenix.Endpoint.CowboyAdapter` for Cowboy 1 is deprecated. Please make sure `{:plug_cowboy, "~> 2.1"}` or later is listed in your `mix.exs`. + * [Endpoint] `Phoenix.Endpoint.instrument/4` is deprecated and has no effect. Use `:telemetry` instead. See `Phoenix.Logger` for more information. + * [Endpoint] The `:pubsub` key for endpoint is deprecated. Once you start your app, you will see step-by-step instructions on how to use the new PubSub config. * [Layout] Use `<%= @inner_content %>` instead of `<%= render @view_module, @view_template, assigns %>` for rendering the child layout ### phx.new installer @@ -59,7 +59,7 @@ Phoenix built-in guides have been restructured and revamped, providing a better * Automatically use `System.get_env("MIX_TEST_PARTITION")` in the database name in the test environment for built-in CI test partitioning * Generate a `MyApp.Telemetry` module with examples of Telemetry Metrics you may want to track in your app * Support the `--live` flag for generating apps with out-of-the-box LiveView support - * Include scss support by default when using webpack + * Include SCSS support by default when using webpack ### JavaScript client From 0155d2f409e7a2bacb6945558303406986a8065e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Boyer?= Date: Wed, 13 May 2020 22:49:13 -0400 Subject: [PATCH 046/199] Fix a few minor typos in guides (#3845) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Frédéric Boyer --- guides/deployment/deployment.md | 2 +- guides/deployment/releases.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/deployment/deployment.md b/guides/deployment/deployment.md index fe1d403794..cb287a3dd4 100644 --- a/guides/deployment/deployment.md +++ b/guides/deployment/deployment.md @@ -52,7 +52,7 @@ $ mix phx.digest Check your digested files at "priv/static". ``` -*Note:* the `--prefix` flag on `npm` may not work on Windows. If so, replace the first command by `cd assets && npm run deply && cd ..`. +*Note:* the `--prefix` flag on `npm` may not work on Windows. If so, replace the first command by `cd assets && npm run deploy && cd ..`. And that is it! The first command builds the assets and the second generates digests as well as a cache manifest file so Phoenix can quickly serve assets in production. diff --git a/guides/deployment/releases.md b/guides/deployment/releases.md index 1f4fed47cd..0bd3a5ccb2 100644 --- a/guides/deployment/releases.md +++ b/guides/deployment/releases.md @@ -45,7 +45,7 @@ $ npm run deploy --prefix ./assets $ mix phx.digest ``` -*Note:* the `--prefix` flag on `npm` may not work on Windows. If so, replace the first command by `cd assets && npm run deply && cd ..`. +*Note:* the `--prefix` flag on `npm` may not work on Windows. If so, replace the first command by `cd assets && npm run deploy && cd ..`. And now run `mix release`: From 6c02f6d5761d8fe2f5194039a7d649b627d1b3ff Mon Sep 17 00:00:00 2001 From: Henrik Nyh Date: Sun, 17 May 2020 09:01:37 +0100 Subject: [PATCH 047/199] heroku.md: Setup -> set up (#3850) "setup" is the noun and "set up" is the (phrasal) verb. --- guides/deployment/heroku.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/guides/deployment/heroku.md b/guides/deployment/heroku.md index 5121bbfb89..d6ec9cc33f 100644 --- a/guides/deployment/heroku.md +++ b/guides/deployment/heroku.md @@ -35,7 +35,7 @@ Let's separate this process into a few steps so we can keep track of where we ar - Initialize Git repository - Sign up for Heroku - Install the Heroku Toolbelt -- Create and setup Heroku application +- Create and set up Heroku application - Make our project ready for Heroku - Deploy time! - Useful Heroku commands @@ -68,9 +68,9 @@ Once we have signed up, we can download the correct version of the Heroku Toolbe The Heroku CLI, part of the Toolbelt, is useful to create Heroku applications, list currently running dynos for an existing application, tail logs or run one-off commands (mix tasks for instance). -## Create and Setup Heroku Application +## Create and Set Up Heroku Application -There are two different ways to deploy a Phoenix app on Heroku. We could use Heroku buildpacks or their container stack. The difference between these two approaches is in how we tell Heroku to treat our build. In buildpack case, we need to update our apps configuration on Heroku to use Phoenix/Elixir specific buildpacks. On container approach, we have more control on how we want to setup our app and we can define our container image using `Dockerfile` and `heroku.yml`. This section will explore the buildpack approach. In order to use Dockerfile, it is often recommended to convert our app to use releases, which we will describe later on. +There are two different ways to deploy a Phoenix app on Heroku. We could use Heroku buildpacks or their container stack. The difference between these two approaches is in how we tell Heroku to treat our build. In buildpack case, we need to update our apps configuration on Heroku to use Phoenix/Elixir specific buildpacks. On container approach, we have more control on how we want to set up our app and we can define our container image using `Dockerfile` and `heroku.yml`. This section will explore the buildpack approach. In order to use Dockerfile, it is often recommended to convert our app to use releases, which we will describe later on. ### Create Application @@ -350,11 +350,11 @@ build: DATABASE_URL: $DATABASE_URL ``` -### Setup releases and Dockerfile +### Set up releases and Dockerfile Now we need to define a `Dockerfile` at the root folder of your project that contains your application. We recommend to use releases when doing so, as the release will allow us to build a container with only the parts of Erlang and Elixir we actually use. Follow [the releases docs](releases.html). At the end of the guide, there is a sample Dockerfile file you can use. -Once you have the image definition setup, you can push your app to heroku and you can see it starts building the image and deploy it. +Once you have the image definition set up, you can push your app to heroku and you can see it starts building the image and deploy it. ## Useful Heroku Commands From 4634fff79254cf6b9886d9345884d7cfeea03c7b Mon Sep 17 00:00:00 2001 From: Josef Richter Date: Sun, 17 May 2020 10:00:37 +0200 Subject: [PATCH 048/199] added missing word in sentence (#3851) --- guides/request_lifecycle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/request_lifecycle.md b/guides/request_lifecycle.md index efd4193431..cbef6f3cab 100644 --- a/guides/request_lifecycle.md +++ b/guides/request_lifecycle.md @@ -8,7 +8,7 @@ Let's get on with our first new Phoenix page! ## Adding a new page -When your browser accesses [http://localhost:4000/](http://localhost:4000/), it sends a HTTP request to whatever service is running on that address, in this our Phoenix application. The HTTP request is made of a verb and a path. For example, the following browser requests translate into: +When your browser accesses [http://localhost:4000/](http://localhost:4000/), it sends a HTTP request to whatever service is running on that address, in this case our Phoenix application. The HTTP request is made of a verb and a path. For example, the following browser requests translate into: | Browser address bar | Verb | Path | |:-----------------------------------|:-----|:--------------| From b80d4e16a4d85d4c8c66c5e308d5431db3b3dfdf Mon Sep 17 00:00:00 2001 From: Vince Foley <39946+binaryseed@users.noreply.github.com> Date: Mon, 18 May 2020 09:29:35 -0700 Subject: [PATCH 049/199] Update conn in Cowboy2 Handler (#3853) --- lib/phoenix/endpoint/cowboy2_handler.ex | 7 ++++--- test/phoenix/integration/endpoint_test.exs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/phoenix/endpoint/cowboy2_handler.ex b/lib/phoenix/endpoint/cowboy2_handler.ex index 51b448537c..97fd2aba64 100644 --- a/lib/phoenix/endpoint/cowboy2_handler.ex +++ b/lib/phoenix/endpoint/cowboy2_handler.ex @@ -60,9 +60,10 @@ defmodule Phoenix.Endpoint.Cowboy2Handler do {:plug, conn, handler, opts} -> %{adapter: {@connection, req}} = - conn - |> handler.call(opts) - |> maybe_send(handler) + conn = + conn + |> handler.call(opts) + |> maybe_send(handler) :telemetry.execute( [:plug_adapter, :call, :stop], diff --git a/test/phoenix/integration/endpoint_test.exs b/test/phoenix/integration/endpoint_test.exs index d5a44d43d3..7c2c4f021f 100644 --- a/test/phoenix/integration/endpoint_test.exs +++ b/test/phoenix/integration/endpoint_test.exs @@ -171,7 +171,7 @@ defmodule Phoenix.Integration.EndpointTest do assert_receive {:event, [:plug_adapter, :call, :stop], %{duration: _}, %{ adapter: :phoenix_cowboy, - conn: %{request_path: "/"}, + conn: %{request_path: "/", status: 200}, plug: ProdEndpoint }} From 32228b8617820b8618d188ad08b46ccb060ec60d Mon Sep 17 00:00:00 2001 From: Tan Jay Jun Date: Sat, 16 May 2020 23:43:23 +0800 Subject: [PATCH 050/199] Do not digest Brotli compressed files (#3840) --- lib/phoenix/digester.ex | 2 +- .../fixtures/digest/priv/static/precompressed.js.br | 1 + .../fixtures/digest/priv/static/precompressed.js.gz | 1 + test/phoenix/digester_test.exs | 13 +++++++++++++ 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/digest/priv/static/precompressed.js.br create mode 100644 test/fixtures/digest/priv/static/precompressed.js.gz diff --git a/lib/phoenix/digester.ex b/lib/phoenix/digester.ex index 2c90453aa7..26c898e7da 100644 --- a/lib/phoenix/digester.ex +++ b/lib/phoenix/digester.ex @@ -119,7 +119,7 @@ defmodule Phoenix.Digester do defp compiled_file?(file_path) do Regex.match?(@digested_file_regex, Path.basename(file_path)) || - Path.extname(file_path) == ".gz" || + Path.extname(file_path) in ~w(.gz .br) || Path.basename(file_path) == "cache_manifest.json" end diff --git a/test/fixtures/digest/priv/static/precompressed.js.br b/test/fixtures/digest/priv/static/precompressed.js.br new file mode 100644 index 0000000000..5a090955fe --- /dev/null +++ b/test/fixtures/digest/priv/static/precompressed.js.br @@ -0,0 +1 @@ +Brotli diff --git a/test/fixtures/digest/priv/static/precompressed.js.gz b/test/fixtures/digest/priv/static/precompressed.js.gz new file mode 100644 index 0000000000..d234d64b16 --- /dev/null +++ b/test/fixtures/digest/priv/static/precompressed.js.gz @@ -0,0 +1 @@ +gzip diff --git a/test/phoenix/digester_test.exs b/test/phoenix/digester_test.exs index 03b25c2ab0..37df1b8d62 100644 --- a/test/phoenix/digester_test.exs +++ b/test/phoenix/digester_test.exs @@ -84,6 +84,19 @@ defmodule Phoenix.DigesterTest do "93pY5dBa8nHHi0Zfj75O/vXCBXb+UvEVCyU7Yd3pzOJ7o1wkYBWbvs3pVXhBChEmo8MDANT11vsggo2+bnYqoQ==" end + test "excludes compiled files" do + input_path = "test/fixtures/digest/priv/static/" + assert :ok = Phoenix.Digester.compile(input_path, @output_path) + output_files = assets_files(@output_path) + + json = Path.join(@output_path, "cache_manifest.json") |> json_read!() + refute json["latest"]["precompressed.js.gz"] + refute json["latest"]["precompressed.js.br"] + + assert :ok = Phoenix.Digester.compile(@output_path, @output_path) + assert output_files == assets_files(@output_path) + end + test "old versions maintain their mtime" do source_path = "test/fixtures/digest/priv/static/" input_path = "tmp/digest/static" From d0b7f5cda603f666951cc814bd8f05303b105ac0 Mon Sep 17 00:00:00 2001 From: Michael Anhari Date: Thu, 21 May 2020 15:39:59 -0400 Subject: [PATCH 051/199] Fix a typo in the docs for testing controllers (#3858) --- guides/testing/testing_controllers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/testing/testing_controllers.md b/guides/testing/testing_controllers.md index 965093f877..d586fb7f75 100644 --- a/guides/testing/testing_controllers.md +++ b/guides/testing/testing_controllers.md @@ -34,7 +34,7 @@ defmodule HelloWeb.PostControllerTest do ... ``` -Similar to the `PageControllerTest` that shops with our application, this controller tests uses `use HelloWeb.ConnCase` to setup the testing structure. Then, as usual, it defines some aliases, some module attributes to use throughout testing, and then it starts a series of `describe` blocks, each of them to test a different controller action. +Similar to the `PageControllerTest` that ships with our application, this controller tests uses `use HelloWeb.ConnCase` to setup the testing structure. Then, as usual, it defines some aliases, some module attributes to use throughout testing, and then it starts a series of `describe` blocks, each of them to test a different controller action. ### The index action From 29e86e34ecd5b5b191482a1b9a3ccad9797db473 Mon Sep 17 00:00:00 2001 From: Chris McCord Date: Thu, 21 May 2020 15:49:11 -0400 Subject: [PATCH 052/199] Bump ex_doc --- mix.exs | 2 +- mix.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mix.exs b/mix.exs index f4d78f79cc..9e4eab028f 100644 --- a/mix.exs +++ b/mix.exs @@ -75,7 +75,7 @@ defmodule Phoenix.MixProject do {:phoenix_html, "~> 2.13", optional: true}, # Docs dependencies - {:ex_doc, "~> 0.20", only: :docs}, + {:ex_doc, "~> 0.22", only: :docs}, {:inch_ex, "~> 0.2", only: :docs}, # Test dependencies diff --git a/mix.lock b/mix.lock index f8ceb3aa37..75a08a8bdb 100644 --- a/mix.lock +++ b/mix.lock @@ -1,12 +1,12 @@ %{ "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"}, "cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"}, - "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"}, - "ex_doc": {:hex, :ex_doc, "0.21.3", "857ec876b35a587c5d9148a2512e952e24c24345552259464b98bfbb883c7b42", [:mix], [{:earmark, "~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0db1ee8d1547ab4877c5b5dffc6604ef9454e189928d5ba8967d4a58a801f161"}, + "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"}, + "ex_doc": {:hex, :ex_doc, "0.22.1", "9bb6d51508778193a4ea90fa16eac47f8b67934f33f8271d5e1edec2dc0eee4c", [:mix], [{:earmark, "~> 1.4.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "d957de1b75cb9f78d3ee17820733dc4460114d8b1e11f7ee4fd6546e69b1db60"}, "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm", "d17ac3aa40835f7feb92913fcdf655edea59938fa8b25f5ce5d423e76a695c3b"}, "inch_ex": {:hex, :inch_ex, "0.5.6", "418357418a553baa6d04eccd1b44171936817db61f4c0840112b420b8e378e67", [:mix], [{:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "7123ca0450686a61416a06cd38e26af18fd0f8c1cff5214770a957c6e0724338"}, "jason": {:hex, :jason, "1.0.0", "0f7cfa9bdb23fed721ec05419bcee2b2c21a77e926bce0deda029b5adc716fe2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b96c400e04b7b765c0854c05a4966323e90c0d11fee0483b1567cda079abb205"}, - "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"}, + "makeup": {:hex, :makeup, "1.0.1", "82f332e461dc6c79dbd82fbe2a9c10d48ed07146f0a478286e590c83c52010b5", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "49736fe5b66a08d8575bf5321d716bac5da20c8e6b97714fec2bcd6febcfa1f8"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"}, "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, From 6d6e48a6e1c3e1aac55271bfa5f5c9142071e114 Mon Sep 17 00:00:00 2001 From: Chris McCord Date: Thu, 21 May 2020 16:05:12 -0400 Subject: [PATCH 053/199] Release 1.5.2 --- CHANGELOG.md | 2 +- assets/package.json | 2 +- guides/introduction/installation.md | 2 +- installer/README.md | 2 +- installer/mix.exs | 2 +- mix.exs | 2 +- package.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c0c80424a..6210e56ff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ Then once you start an application, you will get a warning about the `:pubsub` k Phoenix built-in guides have been restructured and revamped, providing a better navigation structure and more content. -## 1.5.2-dev +## 1.5.2 (2020-05-21) ### Enhancements * [Channel] Import `assigns: 2` on channels diff --git a/assets/package.json b/assets/package.json index 81994acded..79c5591be1 100644 --- a/assets/package.json +++ b/assets/package.json @@ -1,6 +1,6 @@ { "name": "phoenix", - "version": "1.5.1", + "version": "1.5.2", "description": "The official JavaScript client for the Phoenix web framework.", "license": "MIT", "main": "./priv/static/phoenix.js", diff --git a/guides/introduction/installation.md b/guides/introduction/installation.md index 33ea90ff3c..63af566554 100644 --- a/guides/introduction/installation.md +++ b/guides/introduction/installation.md @@ -41,7 +41,7 @@ Elixir 1.6.3 Once we have Elixir and Erlang, we are ready to install the Phoenix application generator: ```console -$ mix archive.install hex phx_new 1.5.1 +$ mix archive.install hex phx_new 1.5.2 ``` We will use this generator to generate new applications in the next guide, called [Up and Running](up_and_running.html). diff --git a/installer/README.md b/installer/README.md index e641002297..14a1f579ea 100644 --- a/installer/README.md +++ b/installer/README.md @@ -4,7 +4,7 @@ Provides `phx.new` installer as an archive. To install from hex, run: - $ mix archive.install hex phx_new 1.5.1 + $ mix archive.install hex phx_new 1.5.2 To build and install it locally, ensure any previous archive versions are removed: diff --git a/installer/mix.exs b/installer/mix.exs index 2d5a394816..117c58305e 100644 --- a/installer/mix.exs +++ b/installer/mix.exs @@ -1,7 +1,7 @@ defmodule Phx.New.MixProject do use Mix.Project - @version "1.5.1" + @version "1.5.2" @github_path "phoenixframework/phoenix" @url "https://github.com/#{@github_path}" diff --git a/mix.exs b/mix.exs index 9e4eab028f..2c4e1e1b0d 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Phoenix.MixProject do use Mix.Project - @version "1.5.1" + @version "1.5.2" # If the elixir requirement is updated, we need to make the installer # use at least the minimum requirement used here. Although often the diff --git a/package.json b/package.json index 5be14ba5f0..0206ff2cc7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "phoenix", - "version": "1.5.1", + "version": "1.5.2", "description": "The official JavaScript client for the Phoenix web framework.", "license": "MIT", "main": "./priv/static/phoenix.js", From de71ae1697264f5117438beaf815f35919c80224 Mon Sep 17 00:00:00 2001 From: Chris McCord Date: Thu, 21 May 2020 16:10:23 -0400 Subject: [PATCH 054/199] Bump js build for 1.5.2 --- assets/package-lock.json | 43 ++++++++++++++++------- installer/templates/phx_static/phoenix.js | 2 +- priv/static/phoenix.js | 2 +- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/assets/package-lock.json b/assets/package-lock.json index a70e86d56a..83698cccaf 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -1,6 +1,6 @@ { "name": "phoenix", - "version": "1.4.0", + "version": "1.5.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -4689,7 +4689,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -4710,12 +4711,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4730,17 +4733,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -4857,7 +4863,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -4869,6 +4876,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4883,6 +4891,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4890,12 +4899,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4914,6 +4925,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -4994,7 +5006,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5006,6 +5019,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5091,7 +5105,8 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5127,6 +5142,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5146,6 +5162,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5189,12 +5206,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/installer/templates/phx_static/phoenix.js b/installer/templates/phx_static/phoenix.js index 692f637f21..6f45d90db7 100644 --- a/installer/templates/phx_static/phoenix.js +++ b/installer/templates/phx_static/phoenix.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Phoenix=t():e.Phoenix=t()}(this,(function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var o=t[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(i,o,function(t){return e[t]}.bind(null,o));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){(function(t){e.exports=t.Phoenix=n(2)}).call(this,n(1))},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";function i(e){return function(e){if(Array.isArray(e))return a(e)}(e)||function(e){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(e))return Array.from(e)}(e)||s(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function r(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(e)))return;var n=[],i=!0,o=!1,r=void 0;try{for(var s,a=e[Symbol.iterator]();!(i=(s=a.next()).done)&&(n.push(s.value),!t||n.length!==t);i=!0);}catch(e){o=!0,r=e}finally{try{i||null==a.return||a.return()}finally{if(o)throw r}}return n}(e,t)||s(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(e,t){if(e){if("string"==typeof e)return a(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(n):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?a(e,t):void 0}}function a(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,i=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:this.timeout;if(this.joinedOnce)throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance");return this.timeout=e,this.joinedOnce=!0,this.rejoin(),this.joinPush}},{key:"onClose",value:function(e){this.on(R,e)}},{key:"onError",value:function(e){return this.on(S,(function(t){return e(t)}))}},{key:"on",value:function(e,t){var n=this.bindingRef++;return this.bindings.push({event:e,ref:n,callback:t}),n}},{key:"off",value:function(e,t){this.bindings=this.bindings.filter((function(n){return!(n.event===e&&(void 0===t||t===n.ref))}))}},{key:"canPush",value:function(){return this.socket.isConnected()&&this.isJoined()}},{key:"push",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:this.timeout;if(!this.joinedOnce)throw new Error("tried to push '".concat(e,"' to '").concat(this.topic,"' before joining. Use channel.join() before pushing events"));var i=new _(this,e,(function(){return t}),n);return this.canPush()?i.send():(i.startTimeout(),this.pushBuffer.push(i)),i}},{key:"leave",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.rejoinTimer.reset(),this.joinPush.cancelTimeout(),this.state=C;var n=function(){e.socket.hasLogger()&&e.socket.log("channel","leave ".concat(e.topic)),e.trigger(R,"leave")},i=new _(this,E,L({}),t);return i.receive("ok",(function(){return n()})).receive("timeout",(function(){return n()})),i.send(),this.canPush()||i.trigger("ok",{}),i}},{key:"onMessage",value:function(e,t,n){return t}},{key:"isLifecycleEvent",value:function(e){return x.indexOf(e)>=0}},{key:"isMember",value:function(e,t,n,i){return this.topic===e&&(!i||i===this.joinRef()||!this.isLifecycleEvent(t)||(this.socket.hasLogger()&&this.socket.log("channel","dropping outdated message",{topic:e,event:t,payload:n,joinRef:i}),!1))}},{key:"joinRef",value:function(){return this.joinPush.ref}},{key:"rejoin",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.isLeaving()||(this.socket.leaveOpenTopic(this.topic),this.state=j,this.joinPush.resend(e))}},{key:"trigger",value:function(e,t,n,i){var o=this.onMessage(e,t,n,i);if(t&&!o)throw new Error("channel onMessage callbacks must return the payload, modified or unmodified");for(var r=this.bindings.filter((function(t){return t.event===e})),s=0;s1&&void 0!==arguments[1]?arguments[1]:{};c(this,e),this.stateChangeCallbacks={open:[],close:[],error:[],message:[]},this.channels=[],this.sendBuffer=[],this.ref=0,this.timeout=i.timeout||1e4,this.transport=i.transport||d.WebSocket||H,this.defaultEncoder=M.encode,this.defaultDecoder=M.decode,this.closeWasClean=!1,this.unloaded=!1,this.binaryType=i.binaryType||"arraybuffer",this.transport!==H?(this.encode=i.encode||this.defaultEncoder,this.decode=i.decode||this.defaultDecoder):(this.encode=this.defaultEncoder,this.decode=this.defaultDecoder),f&&f.addEventListener&&f.addEventListener("unload",(function(e){n.conn&&(n.unloaded=!0,n.abnormalClose("unloaded"))})),this.heartbeatIntervalMs=i.heartbeatIntervalMs||3e4,this.rejoinAfterMs=function(e){return i.rejoinAfterMs?i.rejoinAfterMs(e):[1e3,2e3,5e3][e-1]||1e4},this.reconnectAfterMs=function(e){return n.unloaded?100:i.reconnectAfterMs?i.reconnectAfterMs(e):[10,50,100,150,200,250,500,1e3,2e3][e-1]||5e3},this.logger=i.logger||null,this.longpollerTimeout=i.longpollerTimeout||2e4,this.params=L(i.params||{}),this.endPoint="".concat(t,"/").concat(P),this.vsn=i.vsn||"2.0.0",this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new I((function(){n.teardown((function(){return n.connect()}))}),this.reconnectAfterMs)}return h(e,[{key:"protocol",value:function(){return location.protocol.match(/^https/)?"wss":"ws"}},{key:"endPointURL",value:function(){var e=D.appendParams(D.appendParams(this.endPoint,this.params()),{vsn:this.vsn});return"/"!==e.charAt(0)?e:"/"===e.charAt(1)?"".concat(this.protocol(),":").concat(e):"".concat(this.protocol(),"://").concat(location.host).concat(e)}},{key:"disconnect",value:function(e,t,n){this.closeWasClean=!0,this.reconnectTimer.reset(),this.teardown(e,t,n)}},{key:"connect",value:function(e){var t=this;e&&(console&&console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"),this.params=L(e)),this.conn||(this.closeWasClean=!1,this.conn=new this.transport(this.endPointURL()),this.conn.binaryType=this.binaryType,this.conn.timeout=this.longpollerTimeout,this.conn.onopen=function(){return t.onConnOpen()},this.conn.onerror=function(e){return t.onConnError(e)},this.conn.onmessage=function(e){return t.onConnMessage(e)},this.conn.onclose=function(e){return t.onConnClose(e)})}},{key:"log",value:function(e,t,n){this.logger(e,t,n)}},{key:"hasLogger",value:function(){return null!==this.logger}},{key:"onOpen",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.open.push([t,e]),t}},{key:"onClose",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.close.push([t,e]),t}},{key:"onError",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.error.push([t,e]),t}},{key:"onMessage",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.message.push([t,e]),t}},{key:"onConnOpen",value:function(){this.hasLogger()&&this.log("transport","connected to ".concat(this.endPointURL())),this.unloaded=!1,this.closeWasClean=!1,this.flushSendBuffer(),this.reconnectTimer.reset(),this.resetHeartbeat(),this.stateChangeCallbacks.open.forEach((function(e){return(0,r(e,2)[1])()}))}},{key:"resetHeartbeat",value:function(){var e=this;this.conn&&this.conn.skipHeartbeat||(this.pendingHeartbeatRef=null,clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval((function(){return e.sendHeartbeat()}),this.heartbeatIntervalMs))}},{key:"teardown",value:function(e,t,n){var i=this;if(!this.conn)return e&&e();this.waitForBufferDone((function(){i.conn&&(t?i.conn.close(t,n||""):i.conn.close()),i.waitForSocketClosed((function(){i.conn&&(i.conn.onclose=function(){},i.conn=null),e&&e()}))}))}},{key:"waitForBufferDone",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;5===n||!this.conn||this.conn.bufferedAmount&&0===this.conn.bufferedAmount?e():setTimeout((function(){t.waitForBufferDone(e,n+1)}),150*n)}},{key:"waitForSocketClosed",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;5!==n&&this.conn&&this.conn.readyState!==m?setTimeout((function(){t.waitForSocketClosed(e,n+1)}),150*n):e()}},{key:"onConnClose",value:function(e){this.hasLogger()&&this.log("transport","close",e),this.triggerChanError(),clearInterval(this.heartbeatTimer),this.closeWasClean||this.reconnectTimer.scheduleTimeout(),this.stateChangeCallbacks.close.forEach((function(t){return(0,r(t,2)[1])(e)}))}},{key:"onConnError",value:function(e){this.hasLogger()&&this.log("transport",e),this.triggerChanError(),this.stateChangeCallbacks.error.forEach((function(t){return(0,r(t,2)[1])(e)}))}},{key:"triggerChanError",value:function(){this.channels.forEach((function(e){e.isErrored()||e.isLeaving()||e.isClosed()||e.trigger(S)}))}},{key:"connectionState",value:function(){switch(this.conn&&this.conn.readyState){case p:return"connecting";case v:return"open";case y:return"closing";default:return"closed"}}},{key:"isConnected",value:function(){return"open"===this.connectionState()}},{key:"remove",value:function(e){this.off(e.stateChangeRefs),this.channels=this.channels.filter((function(t){return t.joinRef()!==e.joinRef()}))}},{key:"off",value:function(e){for(var t in this.stateChangeCallbacks)this.stateChangeCallbacks[t]=this.stateChangeCallbacks[t].filter((function(t){var n=r(t,1)[0];return-1===e.indexOf(n)}))}},{key:"channel",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=new A(e,t,this);return this.channels.push(n),n}},{key:"push",value:function(e){var t=this;if(this.hasLogger()){var n=e.topic,i=e.event,o=e.payload,r=e.ref,s=e.join_ref;this.log("push","".concat(n," ").concat(i," (").concat(s,", ").concat(r,")"),o)}this.isConnected()?this.encode(e,(function(e){return t.conn.send(e)})):this.sendBuffer.push((function(){return t.encode(e,(function(e){return t.conn.send(e)}))}))}},{key:"makeRef",value:function(){var e=this.ref+1;return e===this.ref?this.ref=0:this.ref=e,this.ref.toString()}},{key:"sendHeartbeat",value:function(){if(this.isConnected()){if(this.pendingHeartbeatRef)return this.pendingHeartbeatRef=null,this.hasLogger()&&this.log("transport","heartbeat timeout. Attempting to re-establish connection"),void this.abnormalClose("heartbeat timeout");this.pendingHeartbeatRef=this.makeRef(),this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:this.pendingHeartbeatRef})}}},{key:"abnormalClose",value:function(e){this.closeWasClean=!1,this.conn.close(1e3,e)}},{key:"flushSendBuffer",value:function(){this.isConnected()&&this.sendBuffer.length>0&&(this.sendBuffer.forEach((function(e){return e()})),this.sendBuffer=[])}},{key:"onConnMessage",value:function(e){var t=this;this.decode(e.data,(function(e){var n=e.topic,i=e.event,o=e.payload,s=e.ref,a=e.join_ref;s&&s===t.pendingHeartbeatRef&&(t.pendingHeartbeatRef=null),t.hasLogger()&&t.log("receive","".concat(o.status||""," ").concat(n," ").concat(i," ").concat(s&&"("+s+")"||""),o);for(var c=0;c1&&void 0!==arguments[1]?arguments[1]:{};c(this,e);var o=i.events||{state:"presence_state",diff:"presence_diff"};this.state={},this.pendingDiffs=[],this.channel=t,this.joinRef=null,this.caller={onJoin:function(){},onLeave:function(){},onSync:function(){}},this.channel.on(o.state,(function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.joinRef=n.channel.joinRef(),n.state=e.syncState(n.state,t,o,r),n.pendingDiffs.forEach((function(t){n.state=e.syncDiff(n.state,t,o,r)})),n.pendingDiffs=[],s()})),this.channel.on(o.diff,(function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.inPendingSyncState()?n.pendingDiffs.push(t):(n.state=e.syncDiff(n.state,t,o,r),s())}))}return h(e,[{key:"onJoin",value:function(e){this.caller.onJoin=e}},{key:"onLeave",value:function(e){this.caller.onLeave=e}},{key:"onSync",value:function(e){this.caller.onSync=e}},{key:"list",value:function(t){return e.list(this.state,t)}},{key:"inPendingSyncState",value:function(){return!this.joinRef||this.joinRef!==this.channel.joinRef()}}],[{key:"syncState",value:function(e,t,n,i){var o=this,r=this.clone(e),s={},a={};return this.map(r,(function(e,n){t[e]||(a[e]=n)})),this.map(t,(function(e,t){var n=r[e];if(n){var i=t.metas.map((function(e){return e.phx_ref})),c=n.metas.map((function(e){return e.phx_ref})),u=t.metas.filter((function(e){return c.indexOf(e.phx_ref)<0})),h=n.metas.filter((function(e){return i.indexOf(e.phx_ref)<0}));u.length>0&&(s[e]=t,s[e].metas=u),h.length>0&&(a[e]=o.clone(n),a[e].metas=h)}else s[e]=t})),this.syncDiff(r,{joins:s,leaves:a},n,i)}},{key:"syncDiff",value:function(e,t,n,o){var r=t.joins,s=t.leaves,a=this.clone(e);return n||(n=function(){}),o||(o=function(){}),this.map(r,(function(e,t){var o=a[e];if(a[e]=t,o){var r,s=a[e].metas.map((function(e){return e.phx_ref})),c=o.metas.filter((function(e){return s.indexOf(e.phx_ref)<0}));(r=a[e].metas).unshift.apply(r,i(c))}n(e,o,t)})),this.map(s,(function(e,t){var n=a[e];if(n){var i=t.metas.map((function(e){return e.phx_ref}));n.metas=n.metas.filter((function(e){return i.indexOf(e.phx_ref)<0})),o(e,n,t),0===n.metas.length&&delete a[e]}})),a}},{key:"list",value:function(e,t){return t||(t=function(e,t){return t}),this.map(e,(function(e,n){return t(e,n)}))}},{key:"map",value:function(e,t){return Object.getOwnPropertyNames(e).map((function(n){return t(n,e[n])}))}},{key:"clone",value:function(e){return JSON.parse(JSON.stringify(e))}}]),e}(),I=function(){function e(t,n){c(this,e),this.callback=t,this.timerCalc=n,this.timer=null,this.tries=0}return h(e,[{key:"reset",value:function(){this.tries=0,clearTimeout(this.timer)}},{key:"scheduleTimeout",value:function(){var e=this;clearTimeout(this.timer),this.timer=setTimeout((function(){e.tries=e.tries+1,e.callback()}),this.timerCalc(this.tries+1))}}]),e}()}])})); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Phoenix=t():e.Phoenix=t()}(this,(function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var o=t[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(i,o,function(t){return e[t]}.bind(null,o));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){(function(t){e.exports=t.Phoenix=n(2)}).call(this,n(1))},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";function i(e){return function(e){if(Array.isArray(e))return a(e)}(e)||function(e){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(e))return Array.from(e)}(e)||s(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function r(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(e)))return;var n=[],i=!0,o=!1,r=void 0;try{for(var s,a=e[Symbol.iterator]();!(i=(s=a.next()).done)&&(n.push(s.value),!t||n.length!==t);i=!0);}catch(e){o=!0,r=e}finally{try{i||null==a.return||a.return()}finally{if(o)throw r}}return n}(e,t)||s(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(e,t){if(e){if("string"==typeof e)return a(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(n):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?a(e,t):void 0}}function a(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,i=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:this.timeout;if(this.joinedOnce)throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance");return this.timeout=e,this.joinedOnce=!0,this.rejoin(),this.joinPush}},{key:"onClose",value:function(e){this.on(R,e)}},{key:"onError",value:function(e){return this.on(S,(function(t){return e(t)}))}},{key:"on",value:function(e,t){var n=this.bindingRef++;return this.bindings.push({event:e,ref:n,callback:t}),n}},{key:"off",value:function(e,t){this.bindings=this.bindings.filter((function(n){return!(n.event===e&&(void 0===t||t===n.ref))}))}},{key:"canPush",value:function(){return this.socket.isConnected()&&this.isJoined()}},{key:"push",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:this.timeout;if(!this.joinedOnce)throw new Error("tried to push '".concat(e,"' to '").concat(this.topic,"' before joining. Use channel.join() before pushing events"));var i=new _(this,e,(function(){return t}),n);return this.canPush()?i.send():(i.startTimeout(),this.pushBuffer.push(i)),i}},{key:"leave",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.rejoinTimer.reset(),this.joinPush.cancelTimeout(),this.state=C;var n=function(){e.socket.hasLogger()&&e.socket.log("channel","leave ".concat(e.topic)),e.trigger(R,"leave")},i=new _(this,E,L({}),t);return i.receive("ok",(function(){return n()})).receive("timeout",(function(){return n()})),i.send(),this.canPush()||i.trigger("ok",{}),i}},{key:"onMessage",value:function(e,t,n){return t}},{key:"isLifecycleEvent",value:function(e){return x.indexOf(e)>=0}},{key:"isMember",value:function(e,t,n,i){return this.topic===e&&(!i||i===this.joinRef()||!this.isLifecycleEvent(t)||(this.socket.hasLogger()&&this.socket.log("channel","dropping outdated message",{topic:e,event:t,payload:n,joinRef:i}),!1))}},{key:"joinRef",value:function(){return this.joinPush.ref}},{key:"rejoin",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.isLeaving()||(this.socket.leaveOpenTopic(this.topic),this.state=j,this.joinPush.resend(e))}},{key:"trigger",value:function(e,t,n,i){var o=this.onMessage(e,t,n,i);if(t&&!o)throw new Error("channel onMessage callbacks must return the payload, modified or unmodified");for(var r=this.bindings.filter((function(t){return t.event===e})),s=0;s1&&void 0!==arguments[1]?arguments[1]:{};c(this,e),this.stateChangeCallbacks={open:[],close:[],error:[],message:[]},this.channels=[],this.sendBuffer=[],this.ref=0,this.timeout=i.timeout||1e4,this.transport=i.transport||d.WebSocket||H,this.defaultEncoder=M.encode,this.defaultDecoder=M.decode,this.closeWasClean=!1,this.unloaded=!1,this.binaryType=i.binaryType||"arraybuffer",this.transport!==H?(this.encode=i.encode||this.defaultEncoder,this.decode=i.decode||this.defaultDecoder):(this.encode=this.defaultEncoder,this.decode=this.defaultDecoder),f&&f.addEventListener&&f.addEventListener("unload",(function(e){n.conn&&(n.unloaded=!0,n.abnormalClose("unloaded"))})),this.heartbeatIntervalMs=i.heartbeatIntervalMs||3e4,this.rejoinAfterMs=function(e){return i.rejoinAfterMs?i.rejoinAfterMs(e):[1e3,2e3,5e3][e-1]||1e4},this.reconnectAfterMs=function(e){return n.unloaded?100:i.reconnectAfterMs?i.reconnectAfterMs(e):[10,50,100,150,200,250,500,1e3,2e3][e-1]||5e3},this.logger=i.logger||null,this.longpollerTimeout=i.longpollerTimeout||2e4,this.params=L(i.params||{}),this.endPoint="".concat(t,"/").concat(P),this.vsn=i.vsn||"2.0.0",this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new I((function(){n.teardown((function(){return n.connect()}))}),this.reconnectAfterMs)}return h(e,[{key:"protocol",value:function(){return location.protocol.match(/^https/)?"wss":"ws"}},{key:"endPointURL",value:function(){var e=D.appendParams(D.appendParams(this.endPoint,this.params()),{vsn:this.vsn});return"/"!==e.charAt(0)?e:"/"===e.charAt(1)?"".concat(this.protocol(),":").concat(e):"".concat(this.protocol(),"://").concat(location.host).concat(e)}},{key:"disconnect",value:function(e,t,n){this.closeWasClean=!0,this.reconnectTimer.reset(),this.teardown(e,t,n)}},{key:"connect",value:function(e){var t=this;e&&(console&&console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"),this.params=L(e)),this.conn||(this.closeWasClean=!1,this.conn=new this.transport(this.endPointURL()),this.conn.binaryType=this.binaryType,this.conn.timeout=this.longpollerTimeout,this.conn.onopen=function(){return t.onConnOpen()},this.conn.onerror=function(e){return t.onConnError(e)},this.conn.onmessage=function(e){return t.onConnMessage(e)},this.conn.onclose=function(e){return t.onConnClose(e)})}},{key:"log",value:function(e,t,n){this.logger(e,t,n)}},{key:"hasLogger",value:function(){return null!==this.logger}},{key:"onOpen",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.open.push([t,e]),t}},{key:"onClose",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.close.push([t,e]),t}},{key:"onError",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.error.push([t,e]),t}},{key:"onMessage",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.message.push([t,e]),t}},{key:"onConnOpen",value:function(){this.hasLogger()&&this.log("transport","connected to ".concat(this.endPointURL())),this.unloaded=!1,this.closeWasClean=!1,this.flushSendBuffer(),this.reconnectTimer.reset(),this.resetHeartbeat(),this.stateChangeCallbacks.open.forEach((function(e){return(0,r(e,2)[1])()}))}},{key:"resetHeartbeat",value:function(){var e=this;this.conn&&this.conn.skipHeartbeat||(this.pendingHeartbeatRef=null,clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval((function(){return e.sendHeartbeat()}),this.heartbeatIntervalMs))}},{key:"teardown",value:function(e,t,n){var i=this;if(!this.conn)return e&&e();this.waitForBufferDone((function(){i.conn&&(t?i.conn.close(t,n||""):i.conn.close()),i.waitForSocketClosed((function(){i.conn&&(i.conn.onclose=function(){},i.conn=null),e&&e()}))}))}},{key:"waitForBufferDone",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;5!==n&&this.conn&&this.conn.bufferedAmount?setTimeout((function(){t.waitForBufferDone(e,n+1)}),150*n):e()}},{key:"waitForSocketClosed",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;5!==n&&this.conn&&this.conn.readyState!==m?setTimeout((function(){t.waitForSocketClosed(e,n+1)}),150*n):e()}},{key:"onConnClose",value:function(e){this.hasLogger()&&this.log("transport","close",e),this.triggerChanError(),clearInterval(this.heartbeatTimer),this.closeWasClean||this.reconnectTimer.scheduleTimeout(),this.stateChangeCallbacks.close.forEach((function(t){return(0,r(t,2)[1])(e)}))}},{key:"onConnError",value:function(e){this.hasLogger()&&this.log("transport",e),this.triggerChanError(),this.stateChangeCallbacks.error.forEach((function(t){return(0,r(t,2)[1])(e)}))}},{key:"triggerChanError",value:function(){this.channels.forEach((function(e){e.isErrored()||e.isLeaving()||e.isClosed()||e.trigger(S)}))}},{key:"connectionState",value:function(){switch(this.conn&&this.conn.readyState){case p:return"connecting";case v:return"open";case y:return"closing";default:return"closed"}}},{key:"isConnected",value:function(){return"open"===this.connectionState()}},{key:"remove",value:function(e){this.off(e.stateChangeRefs),this.channels=this.channels.filter((function(t){return t.joinRef()!==e.joinRef()}))}},{key:"off",value:function(e){for(var t in this.stateChangeCallbacks)this.stateChangeCallbacks[t]=this.stateChangeCallbacks[t].filter((function(t){var n=r(t,1)[0];return-1===e.indexOf(n)}))}},{key:"channel",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=new A(e,t,this);return this.channels.push(n),n}},{key:"push",value:function(e){var t=this;if(this.hasLogger()){var n=e.topic,i=e.event,o=e.payload,r=e.ref,s=e.join_ref;this.log("push","".concat(n," ").concat(i," (").concat(s,", ").concat(r,")"),o)}this.isConnected()?this.encode(e,(function(e){return t.conn.send(e)})):this.sendBuffer.push((function(){return t.encode(e,(function(e){return t.conn.send(e)}))}))}},{key:"makeRef",value:function(){var e=this.ref+1;return e===this.ref?this.ref=0:this.ref=e,this.ref.toString()}},{key:"sendHeartbeat",value:function(){if(this.isConnected()){if(this.pendingHeartbeatRef)return this.pendingHeartbeatRef=null,this.hasLogger()&&this.log("transport","heartbeat timeout. Attempting to re-establish connection"),void this.abnormalClose("heartbeat timeout");this.pendingHeartbeatRef=this.makeRef(),this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:this.pendingHeartbeatRef})}}},{key:"abnormalClose",value:function(e){this.closeWasClean=!1,this.conn.close(1e3,e)}},{key:"flushSendBuffer",value:function(){this.isConnected()&&this.sendBuffer.length>0&&(this.sendBuffer.forEach((function(e){return e()})),this.sendBuffer=[])}},{key:"onConnMessage",value:function(e){var t=this;this.decode(e.data,(function(e){var n=e.topic,i=e.event,o=e.payload,s=e.ref,a=e.join_ref;s&&s===t.pendingHeartbeatRef&&(t.pendingHeartbeatRef=null),t.hasLogger()&&t.log("receive","".concat(o.status||""," ").concat(n," ").concat(i," ").concat(s&&"("+s+")"||""),o);for(var c=0;c1&&void 0!==arguments[1]?arguments[1]:{};c(this,e);var o=i.events||{state:"presence_state",diff:"presence_diff"};this.state={},this.pendingDiffs=[],this.channel=t,this.joinRef=null,this.caller={onJoin:function(){},onLeave:function(){},onSync:function(){}},this.channel.on(o.state,(function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.joinRef=n.channel.joinRef(),n.state=e.syncState(n.state,t,o,r),n.pendingDiffs.forEach((function(t){n.state=e.syncDiff(n.state,t,o,r)})),n.pendingDiffs=[],s()})),this.channel.on(o.diff,(function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.inPendingSyncState()?n.pendingDiffs.push(t):(n.state=e.syncDiff(n.state,t,o,r),s())}))}return h(e,[{key:"onJoin",value:function(e){this.caller.onJoin=e}},{key:"onLeave",value:function(e){this.caller.onLeave=e}},{key:"onSync",value:function(e){this.caller.onSync=e}},{key:"list",value:function(t){return e.list(this.state,t)}},{key:"inPendingSyncState",value:function(){return!this.joinRef||this.joinRef!==this.channel.joinRef()}}],[{key:"syncState",value:function(e,t,n,i){var o=this,r=this.clone(e),s={},a={};return this.map(r,(function(e,n){t[e]||(a[e]=n)})),this.map(t,(function(e,t){var n=r[e];if(n){var i=t.metas.map((function(e){return e.phx_ref})),c=n.metas.map((function(e){return e.phx_ref})),u=t.metas.filter((function(e){return c.indexOf(e.phx_ref)<0})),h=n.metas.filter((function(e){return i.indexOf(e.phx_ref)<0}));u.length>0&&(s[e]=t,s[e].metas=u),h.length>0&&(a[e]=o.clone(n),a[e].metas=h)}else s[e]=t})),this.syncDiff(r,{joins:s,leaves:a},n,i)}},{key:"syncDiff",value:function(e,t,n,o){var r=t.joins,s=t.leaves,a=this.clone(e);return n||(n=function(){}),o||(o=function(){}),this.map(r,(function(e,t){var o=a[e];if(a[e]=t,o){var r,s=a[e].metas.map((function(e){return e.phx_ref})),c=o.metas.filter((function(e){return s.indexOf(e.phx_ref)<0}));(r=a[e].metas).unshift.apply(r,i(c))}n(e,o,t)})),this.map(s,(function(e,t){var n=a[e];if(n){var i=t.metas.map((function(e){return e.phx_ref}));n.metas=n.metas.filter((function(e){return i.indexOf(e.phx_ref)<0})),o(e,n,t),0===n.metas.length&&delete a[e]}})),a}},{key:"list",value:function(e,t){return t||(t=function(e,t){return t}),this.map(e,(function(e,n){return t(e,n)}))}},{key:"map",value:function(e,t){return Object.getOwnPropertyNames(e).map((function(n){return t(n,e[n])}))}},{key:"clone",value:function(e){return JSON.parse(JSON.stringify(e))}}]),e}(),I=function(){function e(t,n){c(this,e),this.callback=t,this.timerCalc=n,this.timer=null,this.tries=0}return h(e,[{key:"reset",value:function(){this.tries=0,clearTimeout(this.timer)}},{key:"scheduleTimeout",value:function(){var e=this;clearTimeout(this.timer),this.timer=setTimeout((function(){e.tries=e.tries+1,e.callback()}),this.timerCalc(this.tries+1))}}]),e}()}])})); \ No newline at end of file diff --git a/priv/static/phoenix.js b/priv/static/phoenix.js index 692f637f21..6f45d90db7 100644 --- a/priv/static/phoenix.js +++ b/priv/static/phoenix.js @@ -1 +1 @@ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Phoenix=t():e.Phoenix=t()}(this,(function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var o=t[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(i,o,function(t){return e[t]}.bind(null,o));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){(function(t){e.exports=t.Phoenix=n(2)}).call(this,n(1))},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";function i(e){return function(e){if(Array.isArray(e))return a(e)}(e)||function(e){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(e))return Array.from(e)}(e)||s(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function r(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(e)))return;var n=[],i=!0,o=!1,r=void 0;try{for(var s,a=e[Symbol.iterator]();!(i=(s=a.next()).done)&&(n.push(s.value),!t||n.length!==t);i=!0);}catch(e){o=!0,r=e}finally{try{i||null==a.return||a.return()}finally{if(o)throw r}}return n}(e,t)||s(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(e,t){if(e){if("string"==typeof e)return a(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(n):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?a(e,t):void 0}}function a(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,i=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:this.timeout;if(this.joinedOnce)throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance");return this.timeout=e,this.joinedOnce=!0,this.rejoin(),this.joinPush}},{key:"onClose",value:function(e){this.on(R,e)}},{key:"onError",value:function(e){return this.on(S,(function(t){return e(t)}))}},{key:"on",value:function(e,t){var n=this.bindingRef++;return this.bindings.push({event:e,ref:n,callback:t}),n}},{key:"off",value:function(e,t){this.bindings=this.bindings.filter((function(n){return!(n.event===e&&(void 0===t||t===n.ref))}))}},{key:"canPush",value:function(){return this.socket.isConnected()&&this.isJoined()}},{key:"push",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:this.timeout;if(!this.joinedOnce)throw new Error("tried to push '".concat(e,"' to '").concat(this.topic,"' before joining. Use channel.join() before pushing events"));var i=new _(this,e,(function(){return t}),n);return this.canPush()?i.send():(i.startTimeout(),this.pushBuffer.push(i)),i}},{key:"leave",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.rejoinTimer.reset(),this.joinPush.cancelTimeout(),this.state=C;var n=function(){e.socket.hasLogger()&&e.socket.log("channel","leave ".concat(e.topic)),e.trigger(R,"leave")},i=new _(this,E,L({}),t);return i.receive("ok",(function(){return n()})).receive("timeout",(function(){return n()})),i.send(),this.canPush()||i.trigger("ok",{}),i}},{key:"onMessage",value:function(e,t,n){return t}},{key:"isLifecycleEvent",value:function(e){return x.indexOf(e)>=0}},{key:"isMember",value:function(e,t,n,i){return this.topic===e&&(!i||i===this.joinRef()||!this.isLifecycleEvent(t)||(this.socket.hasLogger()&&this.socket.log("channel","dropping outdated message",{topic:e,event:t,payload:n,joinRef:i}),!1))}},{key:"joinRef",value:function(){return this.joinPush.ref}},{key:"rejoin",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.isLeaving()||(this.socket.leaveOpenTopic(this.topic),this.state=j,this.joinPush.resend(e))}},{key:"trigger",value:function(e,t,n,i){var o=this.onMessage(e,t,n,i);if(t&&!o)throw new Error("channel onMessage callbacks must return the payload, modified or unmodified");for(var r=this.bindings.filter((function(t){return t.event===e})),s=0;s1&&void 0!==arguments[1]?arguments[1]:{};c(this,e),this.stateChangeCallbacks={open:[],close:[],error:[],message:[]},this.channels=[],this.sendBuffer=[],this.ref=0,this.timeout=i.timeout||1e4,this.transport=i.transport||d.WebSocket||H,this.defaultEncoder=M.encode,this.defaultDecoder=M.decode,this.closeWasClean=!1,this.unloaded=!1,this.binaryType=i.binaryType||"arraybuffer",this.transport!==H?(this.encode=i.encode||this.defaultEncoder,this.decode=i.decode||this.defaultDecoder):(this.encode=this.defaultEncoder,this.decode=this.defaultDecoder),f&&f.addEventListener&&f.addEventListener("unload",(function(e){n.conn&&(n.unloaded=!0,n.abnormalClose("unloaded"))})),this.heartbeatIntervalMs=i.heartbeatIntervalMs||3e4,this.rejoinAfterMs=function(e){return i.rejoinAfterMs?i.rejoinAfterMs(e):[1e3,2e3,5e3][e-1]||1e4},this.reconnectAfterMs=function(e){return n.unloaded?100:i.reconnectAfterMs?i.reconnectAfterMs(e):[10,50,100,150,200,250,500,1e3,2e3][e-1]||5e3},this.logger=i.logger||null,this.longpollerTimeout=i.longpollerTimeout||2e4,this.params=L(i.params||{}),this.endPoint="".concat(t,"/").concat(P),this.vsn=i.vsn||"2.0.0",this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new I((function(){n.teardown((function(){return n.connect()}))}),this.reconnectAfterMs)}return h(e,[{key:"protocol",value:function(){return location.protocol.match(/^https/)?"wss":"ws"}},{key:"endPointURL",value:function(){var e=D.appendParams(D.appendParams(this.endPoint,this.params()),{vsn:this.vsn});return"/"!==e.charAt(0)?e:"/"===e.charAt(1)?"".concat(this.protocol(),":").concat(e):"".concat(this.protocol(),"://").concat(location.host).concat(e)}},{key:"disconnect",value:function(e,t,n){this.closeWasClean=!0,this.reconnectTimer.reset(),this.teardown(e,t,n)}},{key:"connect",value:function(e){var t=this;e&&(console&&console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"),this.params=L(e)),this.conn||(this.closeWasClean=!1,this.conn=new this.transport(this.endPointURL()),this.conn.binaryType=this.binaryType,this.conn.timeout=this.longpollerTimeout,this.conn.onopen=function(){return t.onConnOpen()},this.conn.onerror=function(e){return t.onConnError(e)},this.conn.onmessage=function(e){return t.onConnMessage(e)},this.conn.onclose=function(e){return t.onConnClose(e)})}},{key:"log",value:function(e,t,n){this.logger(e,t,n)}},{key:"hasLogger",value:function(){return null!==this.logger}},{key:"onOpen",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.open.push([t,e]),t}},{key:"onClose",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.close.push([t,e]),t}},{key:"onError",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.error.push([t,e]),t}},{key:"onMessage",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.message.push([t,e]),t}},{key:"onConnOpen",value:function(){this.hasLogger()&&this.log("transport","connected to ".concat(this.endPointURL())),this.unloaded=!1,this.closeWasClean=!1,this.flushSendBuffer(),this.reconnectTimer.reset(),this.resetHeartbeat(),this.stateChangeCallbacks.open.forEach((function(e){return(0,r(e,2)[1])()}))}},{key:"resetHeartbeat",value:function(){var e=this;this.conn&&this.conn.skipHeartbeat||(this.pendingHeartbeatRef=null,clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval((function(){return e.sendHeartbeat()}),this.heartbeatIntervalMs))}},{key:"teardown",value:function(e,t,n){var i=this;if(!this.conn)return e&&e();this.waitForBufferDone((function(){i.conn&&(t?i.conn.close(t,n||""):i.conn.close()),i.waitForSocketClosed((function(){i.conn&&(i.conn.onclose=function(){},i.conn=null),e&&e()}))}))}},{key:"waitForBufferDone",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;5===n||!this.conn||this.conn.bufferedAmount&&0===this.conn.bufferedAmount?e():setTimeout((function(){t.waitForBufferDone(e,n+1)}),150*n)}},{key:"waitForSocketClosed",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;5!==n&&this.conn&&this.conn.readyState!==m?setTimeout((function(){t.waitForSocketClosed(e,n+1)}),150*n):e()}},{key:"onConnClose",value:function(e){this.hasLogger()&&this.log("transport","close",e),this.triggerChanError(),clearInterval(this.heartbeatTimer),this.closeWasClean||this.reconnectTimer.scheduleTimeout(),this.stateChangeCallbacks.close.forEach((function(t){return(0,r(t,2)[1])(e)}))}},{key:"onConnError",value:function(e){this.hasLogger()&&this.log("transport",e),this.triggerChanError(),this.stateChangeCallbacks.error.forEach((function(t){return(0,r(t,2)[1])(e)}))}},{key:"triggerChanError",value:function(){this.channels.forEach((function(e){e.isErrored()||e.isLeaving()||e.isClosed()||e.trigger(S)}))}},{key:"connectionState",value:function(){switch(this.conn&&this.conn.readyState){case p:return"connecting";case v:return"open";case y:return"closing";default:return"closed"}}},{key:"isConnected",value:function(){return"open"===this.connectionState()}},{key:"remove",value:function(e){this.off(e.stateChangeRefs),this.channels=this.channels.filter((function(t){return t.joinRef()!==e.joinRef()}))}},{key:"off",value:function(e){for(var t in this.stateChangeCallbacks)this.stateChangeCallbacks[t]=this.stateChangeCallbacks[t].filter((function(t){var n=r(t,1)[0];return-1===e.indexOf(n)}))}},{key:"channel",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=new A(e,t,this);return this.channels.push(n),n}},{key:"push",value:function(e){var t=this;if(this.hasLogger()){var n=e.topic,i=e.event,o=e.payload,r=e.ref,s=e.join_ref;this.log("push","".concat(n," ").concat(i," (").concat(s,", ").concat(r,")"),o)}this.isConnected()?this.encode(e,(function(e){return t.conn.send(e)})):this.sendBuffer.push((function(){return t.encode(e,(function(e){return t.conn.send(e)}))}))}},{key:"makeRef",value:function(){var e=this.ref+1;return e===this.ref?this.ref=0:this.ref=e,this.ref.toString()}},{key:"sendHeartbeat",value:function(){if(this.isConnected()){if(this.pendingHeartbeatRef)return this.pendingHeartbeatRef=null,this.hasLogger()&&this.log("transport","heartbeat timeout. Attempting to re-establish connection"),void this.abnormalClose("heartbeat timeout");this.pendingHeartbeatRef=this.makeRef(),this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:this.pendingHeartbeatRef})}}},{key:"abnormalClose",value:function(e){this.closeWasClean=!1,this.conn.close(1e3,e)}},{key:"flushSendBuffer",value:function(){this.isConnected()&&this.sendBuffer.length>0&&(this.sendBuffer.forEach((function(e){return e()})),this.sendBuffer=[])}},{key:"onConnMessage",value:function(e){var t=this;this.decode(e.data,(function(e){var n=e.topic,i=e.event,o=e.payload,s=e.ref,a=e.join_ref;s&&s===t.pendingHeartbeatRef&&(t.pendingHeartbeatRef=null),t.hasLogger()&&t.log("receive","".concat(o.status||""," ").concat(n," ").concat(i," ").concat(s&&"("+s+")"||""),o);for(var c=0;c1&&void 0!==arguments[1]?arguments[1]:{};c(this,e);var o=i.events||{state:"presence_state",diff:"presence_diff"};this.state={},this.pendingDiffs=[],this.channel=t,this.joinRef=null,this.caller={onJoin:function(){},onLeave:function(){},onSync:function(){}},this.channel.on(o.state,(function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.joinRef=n.channel.joinRef(),n.state=e.syncState(n.state,t,o,r),n.pendingDiffs.forEach((function(t){n.state=e.syncDiff(n.state,t,o,r)})),n.pendingDiffs=[],s()})),this.channel.on(o.diff,(function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.inPendingSyncState()?n.pendingDiffs.push(t):(n.state=e.syncDiff(n.state,t,o,r),s())}))}return h(e,[{key:"onJoin",value:function(e){this.caller.onJoin=e}},{key:"onLeave",value:function(e){this.caller.onLeave=e}},{key:"onSync",value:function(e){this.caller.onSync=e}},{key:"list",value:function(t){return e.list(this.state,t)}},{key:"inPendingSyncState",value:function(){return!this.joinRef||this.joinRef!==this.channel.joinRef()}}],[{key:"syncState",value:function(e,t,n,i){var o=this,r=this.clone(e),s={},a={};return this.map(r,(function(e,n){t[e]||(a[e]=n)})),this.map(t,(function(e,t){var n=r[e];if(n){var i=t.metas.map((function(e){return e.phx_ref})),c=n.metas.map((function(e){return e.phx_ref})),u=t.metas.filter((function(e){return c.indexOf(e.phx_ref)<0})),h=n.metas.filter((function(e){return i.indexOf(e.phx_ref)<0}));u.length>0&&(s[e]=t,s[e].metas=u),h.length>0&&(a[e]=o.clone(n),a[e].metas=h)}else s[e]=t})),this.syncDiff(r,{joins:s,leaves:a},n,i)}},{key:"syncDiff",value:function(e,t,n,o){var r=t.joins,s=t.leaves,a=this.clone(e);return n||(n=function(){}),o||(o=function(){}),this.map(r,(function(e,t){var o=a[e];if(a[e]=t,o){var r,s=a[e].metas.map((function(e){return e.phx_ref})),c=o.metas.filter((function(e){return s.indexOf(e.phx_ref)<0}));(r=a[e].metas).unshift.apply(r,i(c))}n(e,o,t)})),this.map(s,(function(e,t){var n=a[e];if(n){var i=t.metas.map((function(e){return e.phx_ref}));n.metas=n.metas.filter((function(e){return i.indexOf(e.phx_ref)<0})),o(e,n,t),0===n.metas.length&&delete a[e]}})),a}},{key:"list",value:function(e,t){return t||(t=function(e,t){return t}),this.map(e,(function(e,n){return t(e,n)}))}},{key:"map",value:function(e,t){return Object.getOwnPropertyNames(e).map((function(n){return t(n,e[n])}))}},{key:"clone",value:function(e){return JSON.parse(JSON.stringify(e))}}]),e}(),I=function(){function e(t,n){c(this,e),this.callback=t,this.timerCalc=n,this.timer=null,this.tries=0}return h(e,[{key:"reset",value:function(){this.tries=0,clearTimeout(this.timer)}},{key:"scheduleTimeout",value:function(){var e=this;clearTimeout(this.timer),this.timer=setTimeout((function(){e.tries=e.tries+1,e.callback()}),this.timerCalc(this.tries+1))}}]),e}()}])})); \ No newline at end of file +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Phoenix=t():e.Phoenix=t()}(this,(function(){return function(e){var t={};function n(i){if(t[i])return t[i].exports;var o=t[i]={i:i,l:!1,exports:{}};return e[i].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,i){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:i})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var i=Object.create(null);if(n.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(i,o,function(t){return e[t]}.bind(null,o));return i},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=0)}([function(e,t,n){(function(t){e.exports=t.Phoenix=n(2)}).call(this,n(1))},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){"use strict";function i(e){return function(e){if(Array.isArray(e))return a(e)}(e)||function(e){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(e))return Array.from(e)}(e)||s(e)||function(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function o(e){return(o="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function r(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if("undefined"==typeof Symbol||!(Symbol.iterator in Object(e)))return;var n=[],i=!0,o=!1,r=void 0;try{for(var s,a=e[Symbol.iterator]();!(i=(s=a.next()).done)&&(n.push(s.value),!t||n.length!==t);i=!0);}catch(e){o=!0,r=e}finally{try{i||null==a.return||a.return()}finally{if(o)throw r}}return n}(e,t)||s(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(e,t){if(e){if("string"==typeof e)return a(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(n):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?a(e,t):void 0}}function a(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,i=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:this.timeout;if(this.joinedOnce)throw new Error("tried to join multiple times. 'join' can only be called a single time per channel instance");return this.timeout=e,this.joinedOnce=!0,this.rejoin(),this.joinPush}},{key:"onClose",value:function(e){this.on(R,e)}},{key:"onError",value:function(e){return this.on(S,(function(t){return e(t)}))}},{key:"on",value:function(e,t){var n=this.bindingRef++;return this.bindings.push({event:e,ref:n,callback:t}),n}},{key:"off",value:function(e,t){this.bindings=this.bindings.filter((function(n){return!(n.event===e&&(void 0===t||t===n.ref))}))}},{key:"canPush",value:function(){return this.socket.isConnected()&&this.isJoined()}},{key:"push",value:function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:this.timeout;if(!this.joinedOnce)throw new Error("tried to push '".concat(e,"' to '").concat(this.topic,"' before joining. Use channel.join() before pushing events"));var i=new _(this,e,(function(){return t}),n);return this.canPush()?i.send():(i.startTimeout(),this.pushBuffer.push(i)),i}},{key:"leave",value:function(){var e=this,t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.rejoinTimer.reset(),this.joinPush.cancelTimeout(),this.state=C;var n=function(){e.socket.hasLogger()&&e.socket.log("channel","leave ".concat(e.topic)),e.trigger(R,"leave")},i=new _(this,E,L({}),t);return i.receive("ok",(function(){return n()})).receive("timeout",(function(){return n()})),i.send(),this.canPush()||i.trigger("ok",{}),i}},{key:"onMessage",value:function(e,t,n){return t}},{key:"isLifecycleEvent",value:function(e){return x.indexOf(e)>=0}},{key:"isMember",value:function(e,t,n,i){return this.topic===e&&(!i||i===this.joinRef()||!this.isLifecycleEvent(t)||(this.socket.hasLogger()&&this.socket.log("channel","dropping outdated message",{topic:e,event:t,payload:n,joinRef:i}),!1))}},{key:"joinRef",value:function(){return this.joinPush.ref}},{key:"rejoin",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this.timeout;this.isLeaving()||(this.socket.leaveOpenTopic(this.topic),this.state=j,this.joinPush.resend(e))}},{key:"trigger",value:function(e,t,n,i){var o=this.onMessage(e,t,n,i);if(t&&!o)throw new Error("channel onMessage callbacks must return the payload, modified or unmodified");for(var r=this.bindings.filter((function(t){return t.event===e})),s=0;s1&&void 0!==arguments[1]?arguments[1]:{};c(this,e),this.stateChangeCallbacks={open:[],close:[],error:[],message:[]},this.channels=[],this.sendBuffer=[],this.ref=0,this.timeout=i.timeout||1e4,this.transport=i.transport||d.WebSocket||H,this.defaultEncoder=M.encode,this.defaultDecoder=M.decode,this.closeWasClean=!1,this.unloaded=!1,this.binaryType=i.binaryType||"arraybuffer",this.transport!==H?(this.encode=i.encode||this.defaultEncoder,this.decode=i.decode||this.defaultDecoder):(this.encode=this.defaultEncoder,this.decode=this.defaultDecoder),f&&f.addEventListener&&f.addEventListener("unload",(function(e){n.conn&&(n.unloaded=!0,n.abnormalClose("unloaded"))})),this.heartbeatIntervalMs=i.heartbeatIntervalMs||3e4,this.rejoinAfterMs=function(e){return i.rejoinAfterMs?i.rejoinAfterMs(e):[1e3,2e3,5e3][e-1]||1e4},this.reconnectAfterMs=function(e){return n.unloaded?100:i.reconnectAfterMs?i.reconnectAfterMs(e):[10,50,100,150,200,250,500,1e3,2e3][e-1]||5e3},this.logger=i.logger||null,this.longpollerTimeout=i.longpollerTimeout||2e4,this.params=L(i.params||{}),this.endPoint="".concat(t,"/").concat(P),this.vsn=i.vsn||"2.0.0",this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new I((function(){n.teardown((function(){return n.connect()}))}),this.reconnectAfterMs)}return h(e,[{key:"protocol",value:function(){return location.protocol.match(/^https/)?"wss":"ws"}},{key:"endPointURL",value:function(){var e=D.appendParams(D.appendParams(this.endPoint,this.params()),{vsn:this.vsn});return"/"!==e.charAt(0)?e:"/"===e.charAt(1)?"".concat(this.protocol(),":").concat(e):"".concat(this.protocol(),"://").concat(location.host).concat(e)}},{key:"disconnect",value:function(e,t,n){this.closeWasClean=!0,this.reconnectTimer.reset(),this.teardown(e,t,n)}},{key:"connect",value:function(e){var t=this;e&&(console&&console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"),this.params=L(e)),this.conn||(this.closeWasClean=!1,this.conn=new this.transport(this.endPointURL()),this.conn.binaryType=this.binaryType,this.conn.timeout=this.longpollerTimeout,this.conn.onopen=function(){return t.onConnOpen()},this.conn.onerror=function(e){return t.onConnError(e)},this.conn.onmessage=function(e){return t.onConnMessage(e)},this.conn.onclose=function(e){return t.onConnClose(e)})}},{key:"log",value:function(e,t,n){this.logger(e,t,n)}},{key:"hasLogger",value:function(){return null!==this.logger}},{key:"onOpen",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.open.push([t,e]),t}},{key:"onClose",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.close.push([t,e]),t}},{key:"onError",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.error.push([t,e]),t}},{key:"onMessage",value:function(e){var t=this.makeRef();return this.stateChangeCallbacks.message.push([t,e]),t}},{key:"onConnOpen",value:function(){this.hasLogger()&&this.log("transport","connected to ".concat(this.endPointURL())),this.unloaded=!1,this.closeWasClean=!1,this.flushSendBuffer(),this.reconnectTimer.reset(),this.resetHeartbeat(),this.stateChangeCallbacks.open.forEach((function(e){return(0,r(e,2)[1])()}))}},{key:"resetHeartbeat",value:function(){var e=this;this.conn&&this.conn.skipHeartbeat||(this.pendingHeartbeatRef=null,clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval((function(){return e.sendHeartbeat()}),this.heartbeatIntervalMs))}},{key:"teardown",value:function(e,t,n){var i=this;if(!this.conn)return e&&e();this.waitForBufferDone((function(){i.conn&&(t?i.conn.close(t,n||""):i.conn.close()),i.waitForSocketClosed((function(){i.conn&&(i.conn.onclose=function(){},i.conn=null),e&&e()}))}))}},{key:"waitForBufferDone",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;5!==n&&this.conn&&this.conn.bufferedAmount?setTimeout((function(){t.waitForBufferDone(e,n+1)}),150*n):e()}},{key:"waitForSocketClosed",value:function(e){var t=this,n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1;5!==n&&this.conn&&this.conn.readyState!==m?setTimeout((function(){t.waitForSocketClosed(e,n+1)}),150*n):e()}},{key:"onConnClose",value:function(e){this.hasLogger()&&this.log("transport","close",e),this.triggerChanError(),clearInterval(this.heartbeatTimer),this.closeWasClean||this.reconnectTimer.scheduleTimeout(),this.stateChangeCallbacks.close.forEach((function(t){return(0,r(t,2)[1])(e)}))}},{key:"onConnError",value:function(e){this.hasLogger()&&this.log("transport",e),this.triggerChanError(),this.stateChangeCallbacks.error.forEach((function(t){return(0,r(t,2)[1])(e)}))}},{key:"triggerChanError",value:function(){this.channels.forEach((function(e){e.isErrored()||e.isLeaving()||e.isClosed()||e.trigger(S)}))}},{key:"connectionState",value:function(){switch(this.conn&&this.conn.readyState){case p:return"connecting";case v:return"open";case y:return"closing";default:return"closed"}}},{key:"isConnected",value:function(){return"open"===this.connectionState()}},{key:"remove",value:function(e){this.off(e.stateChangeRefs),this.channels=this.channels.filter((function(t){return t.joinRef()!==e.joinRef()}))}},{key:"off",value:function(e){for(var t in this.stateChangeCallbacks)this.stateChangeCallbacks[t]=this.stateChangeCallbacks[t].filter((function(t){var n=r(t,1)[0];return-1===e.indexOf(n)}))}},{key:"channel",value:function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=new A(e,t,this);return this.channels.push(n),n}},{key:"push",value:function(e){var t=this;if(this.hasLogger()){var n=e.topic,i=e.event,o=e.payload,r=e.ref,s=e.join_ref;this.log("push","".concat(n," ").concat(i," (").concat(s,", ").concat(r,")"),o)}this.isConnected()?this.encode(e,(function(e){return t.conn.send(e)})):this.sendBuffer.push((function(){return t.encode(e,(function(e){return t.conn.send(e)}))}))}},{key:"makeRef",value:function(){var e=this.ref+1;return e===this.ref?this.ref=0:this.ref=e,this.ref.toString()}},{key:"sendHeartbeat",value:function(){if(this.isConnected()){if(this.pendingHeartbeatRef)return this.pendingHeartbeatRef=null,this.hasLogger()&&this.log("transport","heartbeat timeout. Attempting to re-establish connection"),void this.abnormalClose("heartbeat timeout");this.pendingHeartbeatRef=this.makeRef(),this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:this.pendingHeartbeatRef})}}},{key:"abnormalClose",value:function(e){this.closeWasClean=!1,this.conn.close(1e3,e)}},{key:"flushSendBuffer",value:function(){this.isConnected()&&this.sendBuffer.length>0&&(this.sendBuffer.forEach((function(e){return e()})),this.sendBuffer=[])}},{key:"onConnMessage",value:function(e){var t=this;this.decode(e.data,(function(e){var n=e.topic,i=e.event,o=e.payload,s=e.ref,a=e.join_ref;s&&s===t.pendingHeartbeatRef&&(t.pendingHeartbeatRef=null),t.hasLogger()&&t.log("receive","".concat(o.status||""," ").concat(n," ").concat(i," ").concat(s&&"("+s+")"||""),o);for(var c=0;c1&&void 0!==arguments[1]?arguments[1]:{};c(this,e);var o=i.events||{state:"presence_state",diff:"presence_diff"};this.state={},this.pendingDiffs=[],this.channel=t,this.joinRef=null,this.caller={onJoin:function(){},onLeave:function(){},onSync:function(){}},this.channel.on(o.state,(function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.joinRef=n.channel.joinRef(),n.state=e.syncState(n.state,t,o,r),n.pendingDiffs.forEach((function(t){n.state=e.syncDiff(n.state,t,o,r)})),n.pendingDiffs=[],s()})),this.channel.on(o.diff,(function(t){var i=n.caller,o=i.onJoin,r=i.onLeave,s=i.onSync;n.inPendingSyncState()?n.pendingDiffs.push(t):(n.state=e.syncDiff(n.state,t,o,r),s())}))}return h(e,[{key:"onJoin",value:function(e){this.caller.onJoin=e}},{key:"onLeave",value:function(e){this.caller.onLeave=e}},{key:"onSync",value:function(e){this.caller.onSync=e}},{key:"list",value:function(t){return e.list(this.state,t)}},{key:"inPendingSyncState",value:function(){return!this.joinRef||this.joinRef!==this.channel.joinRef()}}],[{key:"syncState",value:function(e,t,n,i){var o=this,r=this.clone(e),s={},a={};return this.map(r,(function(e,n){t[e]||(a[e]=n)})),this.map(t,(function(e,t){var n=r[e];if(n){var i=t.metas.map((function(e){return e.phx_ref})),c=n.metas.map((function(e){return e.phx_ref})),u=t.metas.filter((function(e){return c.indexOf(e.phx_ref)<0})),h=n.metas.filter((function(e){return i.indexOf(e.phx_ref)<0}));u.length>0&&(s[e]=t,s[e].metas=u),h.length>0&&(a[e]=o.clone(n),a[e].metas=h)}else s[e]=t})),this.syncDiff(r,{joins:s,leaves:a},n,i)}},{key:"syncDiff",value:function(e,t,n,o){var r=t.joins,s=t.leaves,a=this.clone(e);return n||(n=function(){}),o||(o=function(){}),this.map(r,(function(e,t){var o=a[e];if(a[e]=t,o){var r,s=a[e].metas.map((function(e){return e.phx_ref})),c=o.metas.filter((function(e){return s.indexOf(e.phx_ref)<0}));(r=a[e].metas).unshift.apply(r,i(c))}n(e,o,t)})),this.map(s,(function(e,t){var n=a[e];if(n){var i=t.metas.map((function(e){return e.phx_ref}));n.metas=n.metas.filter((function(e){return i.indexOf(e.phx_ref)<0})),o(e,n,t),0===n.metas.length&&delete a[e]}})),a}},{key:"list",value:function(e,t){return t||(t=function(e,t){return t}),this.map(e,(function(e,n){return t(e,n)}))}},{key:"map",value:function(e,t){return Object.getOwnPropertyNames(e).map((function(n){return t(n,e[n])}))}},{key:"clone",value:function(e){return JSON.parse(JSON.stringify(e))}}]),e}(),I=function(){function e(t,n){c(this,e),this.callback=t,this.timerCalc=n,this.timer=null,this.tries=0}return h(e,[{key:"reset",value:function(){this.tries=0,clearTimeout(this.timer)}},{key:"scheduleTimeout",value:function(){var e=this;clearTimeout(this.timer),this.timer=setTimeout((function(){e.tries=e.tries+1,e.callback()}),this.timerCalc(this.tries+1))}}]),e}()}])})); \ No newline at end of file From 099d760eceffd500926885553f9b790cdf59edb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 22 May 2020 02:09:36 +0200 Subject: [PATCH 055/199] Add public_key as an OTP app dep --- mix.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mix.exs b/mix.exs index 2c4e1e1b0d..fbc958c603 100644 --- a/mix.exs +++ b/mix.exs @@ -48,7 +48,7 @@ defmodule Phoenix.MixProject do def application do [ mod: {Phoenix, []}, - extra_applications: [:logger, :eex, :crypto], + extra_applications: [:logger, :eex, :crypto, :public_key], env: [ logger: true, stacktrace_depth: nil, From 9728d3d3c6a9bec4af0e8e70372cc33b8b1292f1 Mon Sep 17 00:00:00 2001 From: Chris McCord Date: Thu, 21 May 2020 21:07:37 -0400 Subject: [PATCH 056/199] Release 1.5.3 to fix incompatible LiveView dep for newly generated projects --- CHANGELOG.md | 5 +++++ assets/package.json | 2 +- guides/introduction/installation.md | 2 +- installer/README.md | 2 +- installer/mix.exs | 2 +- installer/templates/phx_single/mix.exs | 2 +- installer/templates/phx_umbrella/apps/app_name_web/mix.exs | 2 +- mix.exs | 2 +- package.json | 2 +- 9 files changed, 13 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6210e56ff1..b74354331d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ Then once you start an application, you will get a warning about the `:pubsub` k Phoenix built-in guides have been restructured and revamped, providing a better navigation structure and more content. +## 1.5.3 (2020-05-21) + +### Bug fixes + * [phx.new] - Fix incompatible LiveView version for newly generated projects + ## 1.5.2 (2020-05-21) ### Enhancements diff --git a/assets/package.json b/assets/package.json index 79c5591be1..c51d94f2e3 100644 --- a/assets/package.json +++ b/assets/package.json @@ -1,6 +1,6 @@ { "name": "phoenix", - "version": "1.5.2", + "version": "1.5.3", "description": "The official JavaScript client for the Phoenix web framework.", "license": "MIT", "main": "./priv/static/phoenix.js", diff --git a/guides/introduction/installation.md b/guides/introduction/installation.md index 63af566554..7804871f63 100644 --- a/guides/introduction/installation.md +++ b/guides/introduction/installation.md @@ -41,7 +41,7 @@ Elixir 1.6.3 Once we have Elixir and Erlang, we are ready to install the Phoenix application generator: ```console -$ mix archive.install hex phx_new 1.5.2 +$ mix archive.install hex phx_new 1.5.3 ``` We will use this generator to generate new applications in the next guide, called [Up and Running](up_and_running.html). diff --git a/installer/README.md b/installer/README.md index 14a1f579ea..4e8b9d0526 100644 --- a/installer/README.md +++ b/installer/README.md @@ -4,7 +4,7 @@ Provides `phx.new` installer as an archive. To install from hex, run: - $ mix archive.install hex phx_new 1.5.2 + $ mix archive.install hex phx_new 1.5.3 To build and install it locally, ensure any previous archive versions are removed: diff --git a/installer/mix.exs b/installer/mix.exs index 117c58305e..00b7f42f64 100644 --- a/installer/mix.exs +++ b/installer/mix.exs @@ -1,7 +1,7 @@ defmodule Phx.New.MixProject do use Mix.Project - @version "1.5.2" + @version "1.5.3" @github_path "phoenixframework/phoenix" @url "https://github.com/#{@github_path}" diff --git a/installer/templates/phx_single/mix.exs b/installer/templates/phx_single/mix.exs index 7bc051dbe8..79349f623b 100644 --- a/installer/templates/phx_single/mix.exs +++ b/installer/templates/phx_single/mix.exs @@ -41,7 +41,7 @@ defmodule <%= app_module %>.MixProject do {:phoenix_ecto, "~> 4.1"}, {:ecto_sql, "~> 3.4"}, {<%= inspect adapter_app %>, ">= 0.0.0"},<% end %><%= if html do %><%= if live do %> - {:phoenix_live_view, "~> 0.12.0"}, + {:phoenix_live_view, "~> 0.13.0"}, {:floki, ">= 0.0.0", only: :test},<% end %> {:phoenix_html, "~> 2.11"}, {:phoenix_live_reload, "~> 1.2", only: :dev},<% end %><%= if dashboard do %> 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 2ea4511afb..077c2b80a6 100644 --- a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs +++ b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs @@ -39,7 +39,7 @@ defmodule <%= web_namespace %>.MixProject do [ <%= phoenix_dep %>,<%= if ecto do %> {:phoenix_ecto, "~> 4.0"},<% end %><%= if html do %><%= if live do %> - {:phoenix_live_view, "~> 0.12.0"}, + {:phoenix_live_view, "~> 0.13.0"}, {:floki, ">= 0.0.0", only: :test},<% end %> {:phoenix_html, "~> 2.11"}, {:phoenix_live_reload, "~> 1.2", only: :dev},<% end %><%= if dashboard do %> diff --git a/mix.exs b/mix.exs index 2c4e1e1b0d..9bd2d1ca16 100644 --- a/mix.exs +++ b/mix.exs @@ -1,7 +1,7 @@ defmodule Phoenix.MixProject do use Mix.Project - @version "1.5.2" + @version "1.5.3" # If the elixir requirement is updated, we need to make the installer # use at least the minimum requirement used here. Although often the diff --git a/package.json b/package.json index 0206ff2cc7..22a4744cac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "phoenix", - "version": "1.5.2", + "version": "1.5.3", "description": "The official JavaScript client for the Phoenix web framework.", "license": "MIT", "main": "./priv/static/phoenix.js", From 6a34374f14ee0b52289e52ac1685d89e71e3363e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 24 May 2020 10:46:46 +0200 Subject: [PATCH 057/199] Relax dashboard requirement --- installer/templates/phx_single/mix.exs | 2 +- installer/templates/phx_umbrella/apps/app_name_web/mix.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/installer/templates/phx_single/mix.exs b/installer/templates/phx_single/mix.exs index 79349f623b..c2d2c5aefe 100644 --- a/installer/templates/phx_single/mix.exs +++ b/installer/templates/phx_single/mix.exs @@ -45,7 +45,7 @@ defmodule <%= app_module %>.MixProject do {:floki, ">= 0.0.0", only: :test},<% end %> {:phoenix_html, "~> 2.11"}, {:phoenix_live_reload, "~> 1.2", only: :dev},<% end %><%= if dashboard do %> - {:phoenix_live_dashboard, "~> 0.2.0"},<% end %> + {:phoenix_live_dashboard, "~> 0.2"},<% end %> {:telemetry_metrics, "~> 0.4"}, {:telemetry_poller, "~> 0.4"},<%= if gettext do %> {:gettext, "~> 0.11"},<% 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 077c2b80a6..698c5b7065 100644 --- a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs +++ b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs @@ -43,7 +43,7 @@ defmodule <%= web_namespace %>.MixProject do {:floki, ">= 0.0.0", only: :test},<% end %> {:phoenix_html, "~> 2.11"}, {:phoenix_live_reload, "~> 1.2", only: :dev},<% end %><%= if dashboard do %> - {:phoenix_live_dashboard, "~> 0.2.0"},<% end %> + {:phoenix_live_dashboard, "~> 0.2"},<% end %> {:telemetry_metrics, "~> 0.4"}, {:telemetry_poller, "~> 0.4"},<%= if gettext do %> {:gettext, "~> 0.11"},<% end %><%= if app_name != web_app_name do %> From 1b1de30ba763d90e56ab6e1097b16b195ee8bfdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 25 May 2020 11:45:41 +0200 Subject: [PATCH 058/199] Also log when router plugs halt --- lib/phoenix/router.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex index 727dedd0c1..dcc70c93ea 100644 --- a/lib/phoenix/router.ex +++ b/lib/phoenix/router.ex @@ -528,7 +528,7 @@ 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()) + {conn, body} = Plug.Builder.compile(__ENV__, plugs, init_mode: Phoenix.plug_init_mode(), log_on_halt: :debug) quote do defp unquote(name)(unquote(conn)), do: unquote(body) From 8aa9fe5ed02657195edc853d3cc2dd8c79a9b5df Mon Sep 17 00:00:00 2001 From: Tejas Bubane Date: Wed, 27 May 2020 20:16:39 +0530 Subject: [PATCH 059/199] Fix test filename in `testing controllers` doc (#3867) --- guides/testing/testing_controllers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/testing/testing_controllers.md b/guides/testing/testing_controllers.md index d586fb7f75..248eb4481c 100644 --- a/guides/testing/testing_controllers.md +++ b/guides/testing/testing_controllers.md @@ -14,7 +14,7 @@ This gave us a number of modules for free, including a PostController and the as ## HTML controller tests -If you open up "test/hello_web/controllers/post_controller.exs", you will find the following: +If you open up "test/hello_web/controllers/post_controller_test.exs", you will find the following: ```elixir defmodule HelloWeb.PostControllerTest do From 42dbbfafc03a03e240774f4539c8666d00feaaaf Mon Sep 17 00:00:00 2001 From: Harrison Chienjo Date: Wed, 27 May 2020 17:51:02 +0300 Subject: [PATCH 060/199] Fix typos. (#3864) --- guides/howto/custom_error_pages.md | 2 +- guides/request_lifecycle.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/howto/custom_error_pages.md b/guides/howto/custom_error_pages.md index 4bb24de34c..09872b5d2a 100644 --- a/guides/howto/custom_error_pages.md +++ b/guides/howto/custom_error_pages.md @@ -49,7 +49,7 @@ def template_not_found(template, _assigns) do end ``` -Great, so we have this `template_not_found/2` function that takes a template and an `assigns` map, which we ignore. The `template_not_found/2` is invoked whenever a Phoenix.View attempts to render a template bu tno template is found. +Great, so we have this `template_not_found/2` function that takes a template and an `assigns` map, which we ignore. The `template_not_found/2` is invoked whenever a Phoenix.View attempts to render a template but no template is found. In order words, to provide custom error pages, we could simply define a the proper `render/2` function clause in `HelloWeb.ErrorView`. diff --git a/guides/request_lifecycle.md b/guides/request_lifecycle.md index cbef6f3cab..1f5241f522 100644 --- a/guides/request_lifecycle.md +++ b/guides/request_lifecycle.md @@ -147,7 +147,7 @@ which injects our template into the layout before the HTML is sent off to the br As we built our first page, we could start to understand how the request life-cycle is put together. Now let's take a more holistic look at it. -All HTTP requests start in our application endpoint. You can find it as a module named `HelloWeb.Endpoint` in `lib/hello_web/endpoint.ex`. Once you open up the endpoint file, you will see that, similar to the router, the endpoint has many calls to `plug`. `Plug` is a library and specification for stiching web applications together. It is an essential part of how Phoenix handle requests and we will discuss it in detail [in the Plug guide](plug.html) coming next. +All HTTP requests start in our application endpoint. You can find it as a module named `HelloWeb.Endpoint` in `lib/hello_web/endpoint.ex`. Once you open up the endpoint file, you will see that, similar to the router, the endpoint has many calls to `plug`. `Plug` is a library and specification for stitching web applications together. It is an essential part of how Phoenix handle requests and we will discuss it in detail [in the Plug guide](plug.html) coming next. For now, it suffices to say that each Plug defines a slice of request processing. In the endpoint you will find a skeleton roughly like this: From ec01727907ba7ce5498237871b7fadb31b2d8a06 Mon Sep 17 00:00:00 2001 From: Henrik Nyh Date: Wed, 27 May 2020 16:24:46 +0100 Subject: [PATCH 061/199] Resolve conflicts --- lib/phoenix/channel/server.ex | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/phoenix/channel/server.ex b/lib/phoenix/channel/server.ex index a608f4b68d..3099611656 100644 --- a/lib/phoenix/channel/server.ex +++ b/lib/phoenix/channel/server.ex @@ -399,6 +399,22 @@ defmodule Phoenix.Channel.Server do defp init_join(socket, channel, topic) do %{transport_pid: transport_pid, serializer: serializer, pubsub_server: pubsub_server} = socket + + unless pubsub_server do + raise """ + The :pubsub_server was not configured for endpoint #{inspect(socket.endpoint)}. + Make sure to start a PubSub process in your application supervision tree: + + {Phoenix.PubSub, [name: YOURAPP.PubSub, adapter: Phoenix.PubSub.PG2]} + + And then add it to your endpoint config: + + config :YOURAPP, YOURAPPWeb.Endpoint, + # ... + pubsub_server: YOURAPP.PubSub + """ + end + Process.monitor(transport_pid) fastlane = {:fastlane, transport_pid, serializer, channel.__intercepts__()} From aa40c6dd9177411d9b5b027b346bbf625ea25e01 Mon Sep 17 00:00:00 2001 From: Jesse Shieh Date: Wed, 27 May 2020 13:34:48 -0700 Subject: [PATCH 062/199] Gigalixir Deployment Guide (#3870) --- guides/deployment/deployment.md | 4 +- guides/deployment/gigalixir.md | 203 ++++++++++++++++++++++++++++++++ guides/deployment/heroku.md | 2 +- mix.exs | 1 + 4 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 guides/deployment/gigalixir.md diff --git a/guides/deployment/deployment.md b/guides/deployment/deployment.md index cb287a3dd4..51a881f513 100644 --- a/guides/deployment/deployment.md +++ b/guides/deployment/deployment.md @@ -10,7 +10,7 @@ When preparing an application for deployment, there are three main steps: In this guide, we will learn how to get the production environment running locally. You can use the same techniques in this guide to run your application in production, but depending on your deployment infrastructure, extra steps will be necessary. -As an example of deploying to other infrastructures, we also discuss two different approaches in our guides: using [Elixir's releases with `mix release`](releases.html) and [by using Heroku](heroku.html). The release guide also has a sample Docker file you can use if you prefer to deploy with container technologies. +As an example of deploying to other infrastructures, we also discuss three different approaches in our guides: using [Elixir's releases](releases.html) with `mix release`, [using Gigalixir](gigalixir.html), and [using Heroku](heroku.html). The release guide also has a sample Docker file you can use if you prefer to deploy with container technologies. Let's explore those steps above one by one. @@ -112,4 +112,4 @@ $ MIX_ENV=prod mix ecto.migrate $ PORT=4001 MIX_ENV=prod mix phx.server ``` -And that's it. Next you can learn [how to deploy Phoenix with Elixir's releases](releases.html) and [how to deploy to Heroku](heroku.html). +And that's it. Next you can learn how to deploy [with Elixir's releases](releases.html), [to Gigalixir](gigalixir.html), and [to Heroku](heroku.html). diff --git a/guides/deployment/gigalixir.md b/guides/deployment/gigalixir.md new file mode 100644 index 0000000000..ecd7deea20 --- /dev/null +++ b/guides/deployment/gigalixir.md @@ -0,0 +1,203 @@ +# Deploying on Gigalixir + +## What we'll need + +The only thing we'll need for this guide is a working Phoenix application. For those of us who need a simple application to deploy, please follow the [Up and Running guide](https://hexdocs.pm/phoenix/up_and_running.html). + +## Goals + +Our main goal for this guide is to get a Phoenix application running on Gigalixir. + +## Steps + +Let's separate this process into a few steps so we can keep track of where we are. + +- Initialize Git repository +- Install the Gigalixir CLI +- Sign up for Gigalixir +- Create and set up Gigalixir application +- Provision a database +- Make our project ready for Gigalixir +- Deploy time! +- Useful Gigalixir commands + +## Initializing Git repository + +If you haven't already, we'll need to commit our files git. We can do so by running the following commands in our project directory: + +```console +$ git init +$ git add . +$ git commit -m "Initial commit" +``` + +## Installing the Gigalixir CLI + +The Gigalixir command-line interface is installed through `pip`, which is most likely already installed on your system. If it isn't, here are the [pip installation instructions](https://pip.pypa.io/en/stable/installing/) + +Let's install the CLI + +```console +$ pip3 install gigalixir --ignore-installed six +``` + +Next, let's make sure the executable is in your path, if it isn't already + +```console +$ echo 'export PATH=~/.local/bin:$PATH' >> ~/.bash_profile +``` + +Reload your profile + +```console +$ source ~/.bash_profile +``` + +And verify it works + +```console +$ gigalixir --help +``` + +## Signing up for Gigalixir + +We can sign up for an account at [gigalixir.com](https://www.gigalixir.com) or with the CLI. Let's use the CLI. + +```console +$ gigalixir signup +``` + +Gigalixir’s free tier does not require a credit card and comes with 1 app instance and 1 postgresql database for free, but please consider upgrading to a paid plan if you are running a production application. + +Next, let's login + +```console +$ gigalixir login +``` + +And verify + +```console +$ gigalixir account +``` + +## Creating and setting up our Gigalixir application + +There are three different ways to deploy a Phoenix app on Gigalixir: with mix, with Elixir's releases, or with Distillery. In this guide, we'll be using Mix because it is the easiest to get up and running, but you won't be able to connect a remote observer or hot upgrade. For more information, see [Mix vs Distillery vs Elixir Releases](https://gigalixir.readthedocs.io/en/latest/main.html#deploy-types). If you want to deploy with another method, follow the [Getting Started Guide](https://gigalixir.readthedocs.io/en/latest/main.html#getting-started-guide). + +### Creating a Gigalixir application + +Let's create a Gigalixir application + +```console +$ gigalixir create +``` + +Verify it was created + +```console +$ gigalixir apps +``` + +Verify that a git remote was created + +```console +$ git remote -v +``` + +### Specifying versions + +The buildpacks we use default to Elixir, Erlang, and Nodejs versions that are quite old and it's generally a good idea to run the same version in production as you do in development, so let's do that. + +```console +$ echo "elixir_version=1.10.3" > elixir_buildpack.config +$ echo "erlang_version=22.3" >> elixir_buildpack.config +$ echo "node_version=12.16.3" > phoenix_static_buildpack.config +``` + +Don't forget to commit + +```console +$ git add elixir_buildpack.config phoenix_static_buildpack.config +$ git commit -m "set elixir, erlang, and node version" +``` +## Making our Project ready for Gigalixir + +There's nothing we need to do to get our app running on Giglaixir, but for a production app, you probably want to enforce SSL. To do that, see [Force SSL](https://hexdocs.pm/phoenix/using_ssl.html#force-ssl) + +You may also want to use SSL for your database connection. For that, uncomment the line `ssl: true` in your `Repo` config. + +## Provisioning a database + +Let's provision a database for our app + +```console +$ gigalixir pg:create --free +``` + +Verify the database was created + +```console +$ gigalixir pg +``` + +Verify that a `DATABASE_URL` and `POOL_SIZE` were created + +```console +$ gigalixir config +``` + +## Deploy Time! + +Our project is now ready to be deployed on Gigalixir. + +```console +$ git push gigalixir master +``` + +Check the status of your deploy and wait until the app is `Healthy` + +```console +$ gigalixir ps +``` + +Run migrations + +```console +$ gigalixir run mix ecto.migrate +``` + +Check your app logs + +```console +$ gigalixir logs +``` + +If everything looks good, let's take a look at your app running on Gigalixir + +```console +$ gigalixir open +``` + +## Useful Gigalixir Commands + +Open a remote console + +```console +$ gigalixir account:ssh_keys:add "$(cat ~/.ssh/id_rsa.pub)" +$ gigalixir ps:remote_console +``` + +To open a remote observer, see [Remote Observer](https://gigalixir.readthedocs.io/en/latest/main.html#observer) + +To set up clustering, see [Clustering Nodes](https://gigalixir.readthedocs.io/en/latest/main.html#clustering) + +To hot upgrade, see [Hot Upgrades](https://gigalixir.readthedocs.io/en/latest/main.html#hot-upgrade) + +For custom domains, scaling, jobs and other features, see the [Gigalixir Documentation](https://gigalixir.readthedocs.io/) + +## Troubleshooting + +See [Troubleshooting](https://gigalixir.readthedocs.io/en/latest/main.html#troubleshooting) + +Also, don't hesitate to email [help@gigalixir.com](mailto:help@gigalixir.com) or [request an invitation](https://elixir-slackin.herokuapp.com/) and join the #gigalixir channel on [Slack](https://elixir-lang.slack.com). diff --git a/guides/deployment/heroku.md b/guides/deployment/heroku.md index d6ec9cc33f..b231fdf20f 100644 --- a/guides/deployment/heroku.md +++ b/guides/deployment/heroku.md @@ -26,7 +26,7 @@ Heroku is a great platform and Elixir performs well on it. However, you may run If you are just getting started or you don't expect to use the features above, Heroku should be enough for your needs. For instance, if you are migrating an existing application running on Heroku to Phoenix, keeping a similar set of features, Elixir will perform just as well or even better than your current stack. -If you want a platform-as-a-service without these limitations, try [Gigalixir](http://gigalixir.readthedocs.io/). If you would rather deploy to a cloud platform, such as EC2, Google Cloud, etc, consider [Distillery](https://github.com/bitwalker/distillery). +If you want a platform-as-a-service without these limitations, try [Gigalixir](gigalixir.html). If you would rather deploy to a cloud platform, such as EC2, Google Cloud, etc, consider [Distillery](https://github.com/bitwalker/distillery). ## Steps diff --git a/mix.exs b/mix.exs index 1502018545..fc7739c5e9 100644 --- a/mix.exs +++ b/mix.exs @@ -144,6 +144,7 @@ defmodule Phoenix.MixProject do "guides/deployment/deployment.md", "guides/deployment/releases.md", + "guides/deployment/gigalixir.md", "guides/deployment/heroku.md", "guides/howto/custom_error_pages.md", From 6320e0014813af19beb92d5c23ddf02c73ad6051 Mon Sep 17 00:00:00 2001 From: Adam Ho Date: Thu, 28 May 2020 21:01:00 +0800 Subject: [PATCH 063/199] fix: minor typo (#3873) --- guides/request_lifecycle.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/request_lifecycle.md b/guides/request_lifecycle.md index 1f5241f522..b7c6d2cbda 100644 --- a/guides/request_lifecycle.md +++ b/guides/request_lifecycle.md @@ -172,7 +172,7 @@ At this moment, you may be thinking this can be a lot of steps to simply render * endpoint (`Phoenix.Endpoint`) - the endpoint contains the common and initial path that all requests go through. If you want something to happen on all requests, it goes to the endpoint - * router (`Phoenix.Router`) - the router is responsible for dispatching verb/patch to controllers. The router also allows us to scope functionality. For example, some pages in your application may require user authentication, others may not + * router (`Phoenix.Router`) - the router is responsible for dispatching verb/path to controllers. The router also allows us to scope functionality. For example, some pages in your application may require user authentication, others may not * controller (`Phoenix.Controller`) - the job of the controller is to retrieve request information, talk to your business domain, and prepare data for the presentation layer From 32a467881d984f5cad0e004aa51b35ee9a77c63f Mon Sep 17 00:00:00 2001 From: kenny Date: Wed, 27 May 2020 09:46:51 -0600 Subject: [PATCH 064/199] add example for channel.push (#3827) * add example for channel.push Co-authored-by: Chris McCord --- assets/js/phoenix.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/assets/js/phoenix.js b/assets/js/phoenix.js index 0c2e579ad0..a1297d5901 100644 --- a/assets/js/phoenix.js +++ b/assets/js/phoenix.js @@ -503,6 +503,16 @@ export class Channel { canPush(){ return this.socket.isConnected() && this.isJoined() } /** + * Sends a message `event` to phoenix with the payload `payload`. + * Phoenix receives this in the `handle_in(event, payload, socket)` + * function. if phoenix replies or it times out (default 10000ms), + * then optionally the reply can be received. + * + * @example + * channel.push("event") + * .receive("ok", payload => console.log("phoenix replied:", payload)) + * .receive("error", err => console.log("phoenix errored", err)) + * .receive("timeout", () => console.log("timed out pushing")) * @param {string} event * @param {Object} payload * @param {number} [timeout] From 3f4a68a9552eaceed197f6947ab2c7e7b42d5631 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 2 Jun 2020 01:26:24 +0200 Subject: [PATCH 065/199] Remove cowboy adapter events --- lib/phoenix/logger.ex | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/lib/phoenix/logger.ex b/lib/phoenix/logger.ex index 3460a55b1d..2a11d3da5c 100644 --- a/lib/phoenix/logger.ex +++ b/lib/phoenix/logger.ex @@ -7,24 +7,6 @@ defmodule Phoenix.Logger do Phoenix uses the `:telemetry` library for instrumentation. The following events are published by Phoenix with the following measurements and metadata: - * `[:plug_adapter, :call, :start]` - dispatched by Phoenix integration with Cowboy, - this includes all requests, such as assets and web socket connections - * Measurement: `%{system_time: system_time}` - * Metadata: `%{conn: Plug.Conn.t, plug: endpoint}` - * Disable logging: not logged by default - - * `[:plug_adapter, :call, :stop]` - dispatched by Phoenix integration with Cowboy, - whenever your endpoint finishes processing successfully - * Measurement: `%{duration: native_time}` - * Metadata: `%{conn: Plug.Conn.t, plug: endpoint}` - * Disable logging: not logged by default - - * `[:plug_adapter, :call, :exception]` - dispatched by Phoenix integration with Cowboy, - whenever there are errors - * Measurement: `%{duration: native_time}` - * Metadata: `%{conn: Plug.Conn.t, plug: endpoint, kind: kind, reason: reason, stacktrace: stacktrace}` - * Disable logging: not logged by default - * `[:phoenix, :endpoint, :start]` - dispatched by `Plug.Telemetry` in your endpoint, usually after code reloading * Measurement: `%{system_time: system_time}` From 32ae7c368febb73675c7585629ab591ce0f3705d Mon Sep 17 00:00:00 2001 From: Jesse Shieh Date: Tue, 2 Jun 2020 11:08:19 -0700 Subject: [PATCH 066/199] Updated links to Gigalixir Documentation (#3879) --- guides/deployment/gigalixir.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/guides/deployment/gigalixir.md b/guides/deployment/gigalixir.md index ecd7deea20..8a178de3f7 100644 --- a/guides/deployment/gigalixir.md +++ b/guides/deployment/gigalixir.md @@ -83,7 +83,7 @@ $ gigalixir account ## Creating and setting up our Gigalixir application -There are three different ways to deploy a Phoenix app on Gigalixir: with mix, with Elixir's releases, or with Distillery. In this guide, we'll be using Mix because it is the easiest to get up and running, but you won't be able to connect a remote observer or hot upgrade. For more information, see [Mix vs Distillery vs Elixir Releases](https://gigalixir.readthedocs.io/en/latest/main.html#deploy-types). If you want to deploy with another method, follow the [Getting Started Guide](https://gigalixir.readthedocs.io/en/latest/main.html#getting-started-guide). +There are three different ways to deploy a Phoenix app on Gigalixir: with mix, with Elixir's releases, or with Distillery. In this guide, we'll be using Mix because it is the easiest to get up and running, but you won't be able to connect a remote observer or hot upgrade. For more information, see [Mix vs Distillery vs Elixir Releases](https://gigalixir.readthedocs.io/en/latest/modify-app/index.html#mix-vs-distillery-vs-elixir-releases). If you want to deploy with another method, follow the [Getting Started Guide](https://gigalixir.readthedocs.io/en/latest/getting-started-guide.html). ### Creating a Gigalixir application @@ -188,16 +188,16 @@ $ gigalixir account:ssh_keys:add "$(cat ~/.ssh/id_rsa.pub)" $ gigalixir ps:remote_console ``` -To open a remote observer, see [Remote Observer](https://gigalixir.readthedocs.io/en/latest/main.html#observer) +To open a remote observer, see [Remote Observer](https://gigalixir.readthedocs.io/en/latest/runtime.html#how-to-launch-a-remote-observer) -To set up clustering, see [Clustering Nodes](https://gigalixir.readthedocs.io/en/latest/main.html#clustering) +To set up clustering, see [Clustering Nodes](https://gigalixir.readthedocs.io/en/latest/cluster.html) -To hot upgrade, see [Hot Upgrades](https://gigalixir.readthedocs.io/en/latest/main.html#hot-upgrade) +To hot upgrade, see [Hot Upgrades](https://gigalixir.readthedocs.io/en/latest/deploy.html#how-to-hot-upgrade-an-app) For custom domains, scaling, jobs and other features, see the [Gigalixir Documentation](https://gigalixir.readthedocs.io/) ## Troubleshooting -See [Troubleshooting](https://gigalixir.readthedocs.io/en/latest/main.html#troubleshooting) +See [Troubleshooting](https://gigalixir.readthedocs.io/en/latest/troubleshooting.html) Also, don't hesitate to email [help@gigalixir.com](mailto:help@gigalixir.com) or [request an invitation](https://elixir-slackin.herokuapp.com/) and join the #gigalixir channel on [Slack](https://elixir-lang.slack.com). From d9edfb014238cbe4815dcb27aac967b7c8323444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sadjow=20Le=C3=A3o?= Date: Tue, 2 Jun 2020 15:29:04 -0300 Subject: [PATCH 067/199] Clarify usage of enableLatencySim in app.js (#3875) * Clarify usage of enableLatencySim in app.js Co-authored-by: Chris McCord --- installer/templates/phx_assets/app.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/installer/templates/phx_assets/app.js b/installer/templates/phx_assets/app.js index 7467d54e38..0d0d10fff6 100644 --- a/installer/templates/phx_assets/app.js +++ b/installer/templates/phx_assets/app.js @@ -29,6 +29,7 @@ liveSocket.connect() // expose liveSocket on window for web console debug logs and latency simulation: // >> liveSocket.enableDebug() -// >> liveSocket.enableLatencySim(1000) +// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session +// >> liveSocket.disableLatencySim() window.liveSocket = liveSocket -<% end %> \ No newline at end of file +<% end %> From 34cb650f1a633d62043969f660cc08ddd546bccb Mon Sep 17 00:00:00 2001 From: Henrik Nyh Date: Mon, 8 Jun 2020 11:27:06 +0100 Subject: [PATCH 068/199] phoenix.js: typo (#3882) --- assets/js/phoenix.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/js/phoenix.js b/assets/js/phoenix.js index a1297d5901..9e26ac5d75 100644 --- a/assets/js/phoenix.js +++ b/assets/js/phoenix.js @@ -540,7 +540,7 @@ export class Channel { * * Triggers onClose() hooks * - * To receive leave acknowledgements, use the a `receive` + * To receive leave acknowledgements, use the `receive` * hook to bind to the server ack, ie: * * @example From 1328b06344b2ec0dfbb8732d34881c0039da6f18 Mon Sep 17 00:00:00 2001 From: Wes Harper Date: Mon, 8 Jun 2020 04:24:20 -0600 Subject: [PATCH 069/199] Fix typo in documentation (#3890) * fix typo * fix grammatical error * fix grammar" * fix grammar --- guides/directory_structure.md | 4 ++-- guides/request_lifecycle.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/guides/directory_structure.md b/guides/directory_structure.md index cf34fe4a6d..b348e68b1e 100644 --- a/guides/directory_structure.md +++ b/guides/directory_structure.md @@ -28,7 +28,7 @@ We will go over those directories one by one: * `deps` - a directory with all of our Mix dependencies. You can find all dependencies listed in the `mix.exs` file, inside the `def deps do` function definition. This directory must not be checked into version control and it can be removed at any time. Removing it will force Mix to download all deps from scratch - * `lib` - a directory that holds your application source code. This directory is broken into two subdirectories, `lib/hello` and `lib/hello_web`. The `lib/hello` directory will be responsible to host all of your business logic and business domain. It typically interacts directly with the database - it is the "Model" in Model-View-Controller (MVC) architecture. `lib/hello_web` is responsible for exposing your business domain to the world, in this case, though a web application. It holds both the View and Controller from MVC. We will discuss the contents of these directories with more detail in the next sections + * `lib` - a directory that holds your application source code. This directory is broken into two subdirectories, `lib/hello` and `lib/hello_web`. The `lib/hello` directory will be responsible to host all of your business logic and business domain. It typically interacts directly with the database - it is the "Model" in Model-View-Controller (MVC) architecture. `lib/hello_web` is responsible for exposing your business domain to the world, in this case, through a web application. It holds both the View and Controller from MVC. We will discuss the contents of these directories with more detail in the next sections * `priv` - a directory that keeps all assets that are necessary in production but are not directly part of your source code. You typically keep database scripts, translation files, and more in here @@ -61,7 +61,7 @@ children = [ ] ``` -If it is your first time with Phoenix, you don't need to worry about the details right now. For now, suffices to say our application starts a database repository, a pubsub system for sharing messages across processes and nodes, and the application endpoint, which effectively serves HTTP requests. These services are started in the order they are defined and, whenever shutting down your application, they are stopped in the reverse order. +If it is your first time with Phoenix, you don't need to worry about the details right now. For now, suffice it to say our application starts a database repository, a pubsub system for sharing messages across processes and nodes, and the application endpoint, which effectively serves HTTP requests. These services are started in the order they are defined and, whenever shutting down your application, they are stopped in the reverse order. You can learn more about applications in [Elixir's official docs for Application](https://hexdocs.pm/elixir/Application.html). diff --git a/guides/request_lifecycle.md b/guides/request_lifecycle.md index b7c6d2cbda..f79bdd99f2 100644 --- a/guides/request_lifecycle.md +++ b/guides/request_lifecycle.md @@ -147,7 +147,7 @@ which injects our template into the layout before the HTML is sent off to the br As we built our first page, we could start to understand how the request life-cycle is put together. Now let's take a more holistic look at it. -All HTTP requests start in our application endpoint. You can find it as a module named `HelloWeb.Endpoint` in `lib/hello_web/endpoint.ex`. Once you open up the endpoint file, you will see that, similar to the router, the endpoint has many calls to `plug`. `Plug` is a library and specification for stitching web applications together. It is an essential part of how Phoenix handle requests and we will discuss it in detail [in the Plug guide](plug.html) coming next. +All HTTP requests start in our application endpoint. You can find it as a module named `HelloWeb.Endpoint` in `lib/hello_web/endpoint.ex`. Once you open up the endpoint file, you will see that, similar to the router, the endpoint has many calls to `plug`. `Plug` is a library and specification for stitching web applications together. It is an essential part of how Phoenix handles requests and we will discuss it in detail [in the Plug guide](plug.html) coming next. For now, it suffices to say that each Plug defines a slice of request processing. In the endpoint you will find a skeleton roughly like this: @@ -166,7 +166,7 @@ defmodule HelloWeb.Endpoint do end ``` -Each of these plugs have a specific responsibility that we will learn later. The last plug is precisely the `HelloWeb.Router` module. This allows the endpoint to delegate all further request processing to the router. As we now know, its main responsibility is to map verb/path pairs to controllers. The controllers then tells a view to render a template. +Each of these plugs have a specific responsibility that we will learn later. The last plug is precisely the `HelloWeb.Router` module. This allows the endpoint to delegate all further request processing to the router. As we now know, its main responsibility is to map verb/path pairs to controllers. The controller then tells a view to render a template. At this moment, you may be thinking this can be a lot of steps to simply render a page. However, as our application grows in complexity, we will see that each layer serves a distinct purpose: From 5eae614ded0d51bfb4c72cb3cef6c04cf6a2971f Mon Sep 17 00:00:00 2001 From: victor-adrian Date: Mon, 8 Jun 2020 14:27:50 -0500 Subject: [PATCH 070/199] Remove extra brace from instructions (#3893) --- installer/lib/mix/tasks/phx.new.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer/lib/mix/tasks/phx.new.ex b/installer/lib/mix/tasks/phx.new.ex index 1779bef642..e4b8d9de30 100644 --- a/installer/lib/mix/tasks/phx.new.ex +++ b/installer/lib/mix/tasks/phx.new.ex @@ -181,7 +181,7 @@ defmodule Mix.Tasks.Phx.New do If you don't plan to define an Ecto app, you must explicitly start the PubSub in your supervision tree as: - {Phoenix.PubSub, name: #{inspect(project.app_mod)}.PubSub}} + {Phoenix.PubSub, name: #{inspect(project.app_mod)}.PubSub} """) end From 53473c4ac9f7e09b2a3450b333e4c6cd96ecdcea Mon Sep 17 00:00:00 2001 From: Bryan Naegele Date: Mon, 8 Jun 2020 13:29:46 -0600 Subject: [PATCH 071/199] Add a link to telemetry events in guide (#3895) --- guides/telemetry.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/guides/telemetry.md b/guides/telemetry.md index 1c11b92361..e5027ac626 100644 --- a/guides/telemetry.md +++ b/guides/telemetry.md @@ -147,6 +147,10 @@ took to get the response: :telemetry.execute([:phoenix, :endpoint, :stop], %{duration: duration}, %{conn: conn}) ``` +### Phoenix Telemetry Events + +A full list of all Phoenix telemetry events can be found in `Phoenix.Logger` + ## Metrics > Metrics are aggregations of Telemetry events with a From f7c57204fb9a60ac3fd43c13a23794d3e1b323ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 10 Jun 2020 19:09:32 +0200 Subject: [PATCH 072/199] Fix telemetry event to match docs and telemetry spec --- lib/phoenix/router.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex index dcc70c93ea..1e3c242171 100644 --- a/lib/phoenix/router.ex +++ b/lib/phoenix/router.ex @@ -359,13 +359,13 @@ defmodule Phoenix.Router do rescue e in Plug.Conn.WrapperError -> measurements = %{duration: System.monotonic_time() - start} - metadata = %{kind: :error, error: e, stacktrace: __STACKTRACE__} + metadata = %{kind: :error, reason: e, stacktrace: __STACKTRACE__} :telemetry.execute([:phoenix, :router_dispatch, :exception], measurements, metadata) Plug.Conn.WrapperError.reraise(e) catch kind, reason -> measurements = %{duration: System.monotonic_time() - start} - metadata = %{kind: kind, error: reason, stacktrace: __STACKTRACE__} + metadata = %{kind: kind, reason: reason, stacktrace: __STACKTRACE__} :telemetry.execute([:phoenix, :router_dispatch, :exception], measurements, metadata) Plug.Conn.WrapperError.reraise(piped_conn, kind, reason, __STACKTRACE__) end From a00e91a8a6b219e23ba0e073f75611d0e683ee40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 10 Jun 2020 19:11:14 +0200 Subject: [PATCH 073/199] Update CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b74354331d..9ec7b2dc10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,14 @@ Then once you start an application, you will get a warning about the `:pubsub` k Phoenix built-in guides have been restructured and revamped, providing a better navigation structure and more content. +### 1.5.4-dev + +### Enhancements + * [Phoenix.Router] Log when router plugs halt + +### Bug fixes + * [Phoenix.Router] Rename the `[:phoenix, :router_dispatch, :exception]` metadata from error to reason to match the documentation and the telemetry specification + ## 1.5.3 (2020-05-21) ### Bug fixes From b2d3e1dd96134b3e5bbda32e19972b1df7a05af2 Mon Sep 17 00:00:00 2001 From: Leandro Cesquini Pereira Date: Sat, 23 May 2020 04:21:10 -0400 Subject: [PATCH 074/199] Warn if the host provided for the Endpoint is invalid (#3863) Related to https://github.com/phoenixframework/phoenix_live_view/issues/911#issuecomment-632905958 --- lib/phoenix/endpoint/supervisor.ex | 9 ++++++--- test/phoenix/endpoint/endpoint_test.exs | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex index bba344ffdc..e017a1868b 100644 --- a/lib/phoenix/endpoint/supervisor.ex +++ b/lib/phoenix/endpoint/supervisor.ex @@ -332,10 +332,9 @@ defmodule Phoenix.Endpoint.Supervisor do end defp build_url(endpoint, url) do - build_url(endpoint.config(:https), endpoint.config(:http), url) - end + https = endpoint.config(:https) + http = endpoint.config(:http) - defp build_url(https, http, url) do {scheme, port} = cond do https -> @@ -350,6 +349,10 @@ defmodule Phoenix.Endpoint.Supervisor do host = host_to_binary(url[:host] || "localhost") port = port_to_integer(url[:port] || port) + if host =~ ":" do + Logger.warn("url: [host: ...] configuration value #{inspect(host)} for #{inspect(endpoint)} is invalid") + end + %URI{scheme: scheme, port: port, host: host} end diff --git a/test/phoenix/endpoint/endpoint_test.exs b/test/phoenix/endpoint/endpoint_test.exs index 26d3846854..a70af1bd36 100644 --- a/test/phoenix/endpoint/endpoint_test.exs +++ b/test/phoenix/endpoint/endpoint_test.exs @@ -10,6 +10,7 @@ defmodule Phoenix.Endpoint.EndpointTest do force_ssl: [subdomains: true], cache_static_manifest: "../../../../test/fixtures/digest/compile/cache_manifest.json", pubsub_server: :endpoint_pub] + Application.put_env(:phoenix, __MODULE__.Endpoint, @config) defmodule Endpoint do From e4ee7cfb76bfec8c00436e158cb03a5248e49ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 10 Jun 2020 19:38:47 +0200 Subject: [PATCH 075/199] Fix tests on latest plug --- test/phoenix/controller/controller_test.exs | 2 +- test/phoenix/test/conn_test.exs | 27 ++++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/test/phoenix/controller/controller_test.exs b/test/phoenix/controller/controller_test.exs index 797427bbcc..9d3761f462 100644 --- a/test/phoenix/controller/controller_test.exs +++ b/test/phoenix/controller/controller_test.exs @@ -569,7 +569,7 @@ defmodule Phoenix.Controller.ControllerTest do end test "keeps structs intact" do - conn = conn(:get, "/", %{"foo" => %{"bar" => %Plug.Upload{}}}) + conn = conn(:post, "/", %{"foo" => %{"bar" => %Plug.Upload{}}}) |> fetch_query_params |> scrub_params("foo") diff --git a/test/phoenix/test/conn_test.exs b/test/phoenix/test/conn_test.exs index e78afde7ca..6c1d4f08a9 100644 --- a/test/phoenix/test/conn_test.exs +++ b/test/phoenix/test/conn_test.exs @@ -1,6 +1,19 @@ defmodule Phoenix.Test.ConnTest.CatchAll do + defmodule ConnError do + defexception [message: "hello", plug_status: 500] + end + def init(opts), do: opts - def call(conn, :stat), do: conn.params["action"].(conn) + + def call(conn, :stat) do + case conn.params["action"] do + "conn_404" -> raise ConnError, plug_status: 404 + "conn_400" -> raise ConnError, plug_status: 400 + "runtime" -> raise RuntimeError + "send_400" -> Plug.Conn.send_resp(conn, 400, "") + end + end + def call(conn, _opts), do: Plug.Conn.assign(conn, :catch_all, true) end @@ -43,10 +56,6 @@ defmodule Phoenix.Test.ConnTest do @moduletag :capture_log - defmodule ConnError do - defexception [message: nil, plug_status: 500] - end - Application.put_env(:phoenix, Phoenix.Test.ConnTest.Endpoint, []) defmodule Endpoint do @@ -520,12 +529,12 @@ defmodule Phoenix.Test.ConnTest do test "assert_error_sent/2 with expected error response" do response = assert_error_sent :not_found, fn -> - get(build_conn(), "/stat", action: fn _ -> raise ConnError, plug_status: 404 end) + get(build_conn(), "/stat", action: "conn_404") end assert {404, [_h | _t], "404.html from Phoenix.ErrorView"} = response response = assert_error_sent 400, fn -> - get(build_conn(), "/stat", action: fn _ -> raise ConnError, plug_status: 400 end) + get(build_conn(), "/stat", action: "conn_400") end assert {400, [_h | _t], "400.html from Phoenix.ErrorView"} = response end @@ -533,7 +542,7 @@ defmodule Phoenix.Test.ConnTest do test "assert_error_sent/2 with status mismatch assertion" do assert_raise ExUnit.AssertionError, ~r/expected error to be sent as 400 status, but got 500 from.*RuntimeError/s, fn -> assert_error_sent 400, fn -> - get(build_conn(), "/stat", action: fn _conn -> raise RuntimeError end) + get(build_conn(), "/stat", action: "runtime") end end end @@ -553,7 +562,7 @@ defmodule Phoenix.Test.ConnTest do test "assert_error_sent/2 with response but no error" do assert_raise ExUnit.AssertionError, ~r/expected error to be sent as 400 status, but response sent 400 without error/, fn -> assert_error_sent :bad_request, fn -> - get(build_conn(), "/stat", action: fn conn -> Plug.Conn.send_resp(conn, 400, "") end) + get(build_conn(), "/stat", action: "send_400") end end end From 6e1941270ba6915e6e003946a60b74f1cf2bb901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 11 Jun 2020 19:34:38 +0200 Subject: [PATCH 076/199] Include connection in error rendered event --- lib/phoenix/endpoint/render_errors.ex | 2 +- lib/phoenix/logger.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/phoenix/endpoint/render_errors.ex b/lib/phoenix/endpoint/render_errors.ex index 41e3f10e1e..6482775532 100644 --- a/lib/phoenix/endpoint/render_errors.ex +++ b/lib/phoenix/endpoint/render_errors.ex @@ -72,7 +72,7 @@ defmodule Phoenix.Endpoint.RenderErrors do status = status(kind, reason) conn = error_conn(conn, kind, reason) start = System.monotonic_time() - metadata = %{status: status, kind: kind, reason: reason, stacktrace: stack, log: level} + metadata = %{conn: conn, status: status, kind: kind, reason: reason, stacktrace: stack, log: level} try do render(conn, status, kind, reason, stack, opts) diff --git a/lib/phoenix/logger.ex b/lib/phoenix/logger.ex index 2a11d3da5c..b5d5cf280c 100644 --- a/lib/phoenix/logger.ex +++ b/lib/phoenix/logger.ex @@ -41,7 +41,7 @@ defmodule Phoenix.Logger do * `[:phoenix, :error_rendered]` - dispatched at the end of an error view being rendered * Measurement: `%{duration: native_time}` - * Metadata: `%{status: Plug.Conn.status, kind: Exception.kind, reason: term, stacktrace: Exception.stacktrace}` + * Metadata: `%{conn: Plug.Conn.t, status: Plug.Conn.status, kind: Exception.kind, reason: term, stacktrace: Exception.stacktrace}` * Disable logging: Set `render_errors: [log: false]` on your endpoint configuration * `[:phoenix, :socket_connected]` - dispatched by `Phoenix.Socket`, at the end of a socket connection From 8819fc332512d79ee2034992c7cd115126336d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 11 Jun 2020 19:36:21 +0200 Subject: [PATCH 077/199] Update plug_cowboy --- mix.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/mix.lock b/mix.lock index 75a08a8bdb..76ad47fc68 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ - "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"}, - "cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"}, + "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, + "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"}, "ex_doc": {:hex, :ex_doc, "0.22.1", "9bb6d51508778193a4ea90fa16eac47f8b67934f33f8271d5e1edec2dc0eee4c", [:mix], [{:earmark, "~> 1.4.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "d957de1b75cb9f78d3ee17820733dc4460114d8b1e11f7ee4fd6546e69b1db60"}, "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm", "d17ac3aa40835f7feb92913fcdf655edea59938fa8b25f5ce5d423e76a695c3b"}, @@ -12,8 +12,8 @@ "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"}, "phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"}, - "plug": {:hex, :plug, "1.10.0", "6508295cbeb4c654860845fb95260737e4a8838d34d115ad76cd487584e2fc4d", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "422a9727e667be1bf5ab1de03be6fa0ad67b775b2d84ed908f3264415ef29d4a"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.2.0", "858dab1b31b17b518270f02285975972a919fa412001e6ae7a199120b113ee24", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ffdbf993cc4e72726c677aafa9fd79bce1474dbfbe523be428d1ae5c7dbdf75b"}, + "plug": {:hex, :plug, "1.10.3", "c9cebe917637d8db0e759039cc106adca069874e1a9034fd6e3fdd427fd3c283", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "01f9037a2a1de1d633b5a881101e6a444bcabb1d386ca1e00bb273a1f1d9d939"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.3.0", "149a50e05cb73c12aad6506a371cd75750c0b19a32f81866e1a323dda9e0e99d", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bc595a1870cef13f9c1e03df56d96804db7f702175e4ccacdb8fc75c02a7b97e"}, "plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, From 80a7cf702b7fff35e15e3a819e9a0e3b74cf35fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 11 Jun 2020 19:37:28 +0200 Subject: [PATCH 078/199] Do not dup stacktraces --- lib/phoenix/endpoint/cowboy2_handler.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/phoenix/endpoint/cowboy2_handler.ex b/lib/phoenix/endpoint/cowboy2_handler.ex index 97fd2aba64..fbf23baa11 100644 --- a/lib/phoenix/endpoint/cowboy2_handler.ex +++ b/lib/phoenix/endpoint/cowboy2_handler.ex @@ -127,15 +127,15 @@ defmodule Phoenix.Endpoint.Cowboy2Handler do defp exit_on_error(:error, value, stack, call) do exception = Exception.normalize(:error, value, stack) - exit({{exception, stack}, call}) + :erlang.raise(:exit, {{exception, stack}, call}, []) end defp exit_on_error(:throw, value, stack, call) do - exit({{{:nocatch, value}, stack}, call}) + :erlang.raise(:exit, {{{:nocatch, value}, stack}, call}, []) end defp exit_on_error(:exit, value, _stack, call) do - exit({value, call}) + :erlang.raise(:exit, {value, call}, []) end defp copy_resp_headers(%Plug.Conn{} = conn, req) do From e5275fabe9933453d0874d706c84fd42a688f285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 11 Jun 2020 20:20:58 +0200 Subject: [PATCH 079/199] Use previous cowboy and cowlib to avoid CI errors on old OTP versions --- mix.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mix.lock b/mix.lock index 76ad47fc68..cbf54b5496 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ - "cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"}, - "cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"}, + "cowboy": {:hex, :cowboy, "2.7.0", "91ed100138a764355f43316b1d23d7ff6bdb0de4ea618cb5d8677c93a7a2f115", [:rebar3], [{:cowlib, "~> 2.8.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "04fd8c6a39edc6aaa9c26123009200fc61f92a3a94f3178c527b70b767c6e605"}, + "cowlib": {:hex, :cowlib, "2.8.0", "fd0ff1787db84ac415b8211573e9a30a3ebe71b5cbff7f720089972b2319c8a4", [:rebar3], [], "hexpm", "79f954a7021b302186a950a32869dbc185523d99d3e44ce430cd1f3289f41ed4"}, "earmark": {:hex, :earmark, "1.4.4", "4821b8d05cda507189d51f2caeef370cf1e18ca5d7dfb7d31e9cafe6688106a4", [:mix], [], "hexpm", "1f93aba7340574847c0f609da787f0d79efcab51b044bb6e242cae5aca9d264d"}, "ex_doc": {:hex, :ex_doc, "0.22.1", "9bb6d51508778193a4ea90fa16eac47f8b67934f33f8271d5e1edec2dc0eee4c", [:mix], [{:earmark, "~> 1.4.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "d957de1b75cb9f78d3ee17820733dc4460114d8b1e11f7ee4fd6546e69b1db60"}, "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm", "d17ac3aa40835f7feb92913fcdf655edea59938fa8b25f5ce5d423e76a695c3b"}, From 112500c88740e63b115a095367bac7e689a3990b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 20 Jun 2020 00:06:44 +0200 Subject: [PATCH 080/199] Remove archive to avoid conflicts, closes #3906 --- test/mix/tasks/phx.new_test.exs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/mix/tasks/phx.new_test.exs b/test/mix/tasks/phx.new_test.exs index c789b5abeb..acfe38b153 100644 --- a/test/mix/tasks/phx.new_test.exs +++ b/test/mix/tasks/phx.new_test.exs @@ -1,3 +1,8 @@ +# Remove archive to avoid conflicts +with list when is_list(list) <- :code.lib_dir(:phx_new) do + :code.del_path(:filename.join(list, 'ebin')) +end + _ = Kernel.ParallelCompiler.compile Path.wildcard("installer/lib/**/*.ex") Code.require_file("installer/test/mix_helper.exs") From 11beaa1f69b30d3398cee88b1038463a64096d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 20 Jun 2020 18:06:36 +0200 Subject: [PATCH 081/199] Update CHANGELOG --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ec7b2dc10..784728e196 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,12 @@ Phoenix built-in guides have been restructured and revamped, providing a better ### 1.5.4-dev ### Enhancements + * [Phoenix.Endpoint] Include `:conn` in `[:phoenix, :error_rendered]` event + * [Phoenix.Endpoint] Warn if the `url.host` provided for the endpoint is invalid * [Phoenix.Router] Log when router plugs halt ### Bug fixes - * [Phoenix.Router] Rename the `[:phoenix, :router_dispatch, :exception]` metadata from error to reason to match the documentation and the telemetry specification + * [Phoenix.Router] Rename the `[:phoenix, :router_dispatch, :exception]` metadata from `:error` to `:reason` to match the documentation and the telemetry specification ## 1.5.3 (2020-05-21) From 149a80818ea1d63346d822fc6df4694e80139f1c Mon Sep 17 00:00:00 2001 From: Jesse Shieh Date: Mon, 22 Jun 2020 09:15:08 -0700 Subject: [PATCH 082/199] Reference Gigalixir docs to install the CLI now that we have different methods of installation for each platform. (#3912) --- guides/deployment/gigalixir.md | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/guides/deployment/gigalixir.md b/guides/deployment/gigalixir.md index 8a178de3f7..e268442c2c 100644 --- a/guides/deployment/gigalixir.md +++ b/guides/deployment/gigalixir.md @@ -33,31 +33,7 @@ $ git commit -m "Initial commit" ## Installing the Gigalixir CLI -The Gigalixir command-line interface is installed through `pip`, which is most likely already installed on your system. If it isn't, here are the [pip installation instructions](https://pip.pypa.io/en/stable/installing/) - -Let's install the CLI - -```console -$ pip3 install gigalixir --ignore-installed six -``` - -Next, let's make sure the executable is in your path, if it isn't already - -```console -$ echo 'export PATH=~/.local/bin:$PATH' >> ~/.bash_profile -``` - -Reload your profile - -```console -$ source ~/.bash_profile -``` - -And verify it works - -```console -$ gigalixir --help -``` +Follow the instructions [here](https://gigalixir.readthedocs.io/en/latest/getting-started-guide.html#install-the-command-line-interface) to install the command-line interface for your platform. ## Signing up for Gigalixir From 8d432e8aae160d9f6b284dd2c374d56d4b861fc2 Mon Sep 17 00:00:00 2001 From: David Oliver Date: Tue, 23 Jun 2020 18:16:08 +0200 Subject: [PATCH 083/199] =?UTF-8?q?Move=20=E2=80=98invalid-feedback?= =?UTF-8?q?=E2=80=99=20style=20selector=20block=20to=20non-LV=20section=20?= =?UTF-8?q?(#3914)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'invalid-feedback' class is used in standard generated forms. --- installer/templates/phx_assets/app.scss | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/installer/templates/phx_assets/app.scss b/installer/templates/phx_assets/app.scss index c1a4f3cb7c..0d4ff2f49e 100644 --- a/installer/templates/phx_assets/app.scss +++ b/installer/templates/phx_assets/app.scss @@ -3,13 +3,8 @@ @import "../node_modules/nprogress/nprogress.css"; /* LiveView specific classes for your customizations */ -.invalid-feedback { - color: #a94442; - display: block; - margin: -1rem 0 2rem; -} - -.phx-no-feedback.invalid-feedback, .phx-no-feedback .invalid-feedback { +.phx-no-feedback.invalid-feedback, +.phx-no-feedback .invalid-feedback { display: none; } @@ -89,3 +84,8 @@ .alert:empty { display: none; } +.invalid-feedback { + color: #a94442; + display: block; + margin: -1rem 0 2rem; +} From 59f1fc23fafaa37d115827e340741bd29571f70d Mon Sep 17 00:00:00 2001 From: Gary Rennie Date: Wed, 24 Jun 2020 08:45:12 +0100 Subject: [PATCH 084/199] Speed up build times for webpack (#3899) On an unmodified default generated project (with --live) using the Phoenix 1.5.3 installer, the build time is: $ webpack --mode development Version: webpack 4.41.5 Time: 1681ms Changing the source-map mode to `eval-cheap-module-source-map` changes this time slightly: Time: 1397ms The `hard-source-webpack-plugin` caches intermediate build steps, which produces significantly faster rebuilds: Time: 474ms Combining these two results in: Time: 193ms This improvement seems to scale well too. On a project including tailwind, the build time by default is close to 6 seconds, but with the above config, 212ms. `eval-cheap-module-source-map` preserves line numbers in the source maps, but not column information. This seems like a resonable trade off vs `eval-module-source-map` which is a bit slower. Time: 491ms --- installer/templates/phx_assets/package.json | 1 + installer/templates/phx_assets/webpack.config.js | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/installer/templates/phx_assets/package.json b/installer/templates/phx_assets/package.json index f33d28a9dd..9ec7d1abfa 100644 --- a/installer/templates/phx_assets/package.json +++ b/installer/templates/phx_assets/package.json @@ -20,6 +20,7 @@ "css-loader": "^3.4.2", "sass-loader": "^8.0.2", "node-sass": "^4.13.1", + "hard-source-webpack-plugin": "^0.13.1", "mini-css-extract-plugin": "^0.9.0", "optimize-css-assets-webpack-plugin": "^5.0.1", "terser-webpack-plugin": "^2.3.2", diff --git a/installer/templates/phx_assets/webpack.config.js b/installer/templates/phx_assets/webpack.config.js index dd77c3dde6..079610dd64 100644 --- a/installer/templates/phx_assets/webpack.config.js +++ b/installer/templates/phx_assets/webpack.config.js @@ -1,5 +1,6 @@ const path = require('path'); const glob = require('glob'); +const HardSourceWebpackPlugin = require('hard-source-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const TerserPlugin = require('terser-webpack-plugin'); const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); @@ -23,7 +24,7 @@ module.exports = (env, options) => { path: path.resolve(__dirname, '../priv/static/js'), publicPath: '/js/' }, - devtool: devMode ? 'source-map' : undefined, + devtool: devMode ? 'eval-cheap-module-source-map' : undefined, module: { rules: [ { @@ -47,5 +48,6 @@ module.exports = (env, options) => { new MiniCssExtractPlugin({ filename: '../css/app.css' }), new CopyWebpackPlugin([{ from: 'static/', to: '../' }]) ] + .concat(devMode ? [new HardSourceWebpackPlugin()] : []) } }; From ffdd60e63d6681ab09d54ab2ac17b41dc3b5b104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 26 Jun 2020 10:08:46 +0200 Subject: [PATCH 085/199] Keep error metadata for backwards compat --- CHANGELOG.md | 2 +- lib/phoenix/router.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 784728e196..2a9f7868e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ Phoenix built-in guides have been restructured and revamped, providing a better * [Phoenix.Router] Log when router plugs halt ### Bug fixes - * [Phoenix.Router] Rename the `[:phoenix, :router_dispatch, :exception]` metadata from `:error` to `:reason` to match the documentation and the telemetry specification + * [Phoenix.Router] Rename the `[:phoenix, :router_dispatch, :exception]` metadata from `:error` to `:reason` to match the documentation and the telemetry specification (`:error` is still emitted for compatibility but it will be fully removed on v1.6) ## 1.5.3 (2020-05-21) diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex index 1e3c242171..ac2aa85d69 100644 --- a/lib/phoenix/router.ex +++ b/lib/phoenix/router.ex @@ -359,7 +359,7 @@ defmodule Phoenix.Router do rescue e in Plug.Conn.WrapperError -> measurements = %{duration: System.monotonic_time() - start} - metadata = %{kind: :error, reason: e, stacktrace: __STACKTRACE__} + metadata = %{kind: :error, error: e, reason: e, stacktrace: __STACKTRACE__} :telemetry.execute([:phoenix, :router_dispatch, :exception], measurements, metadata) Plug.Conn.WrapperError.reraise(e) catch From ee9d90b87df0b282d63548b4582a7b39dc56b196 Mon Sep 17 00:00:00 2001 From: Ahsan Nabi Dar Date: Tue, 30 Jun 2020 17:57:48 +0800 Subject: [PATCH 086/199] Update views.md (#3922) fixed typo --- guides/views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/views.md b/guides/views.md index 3ccd6fc617..ac2ee4e126 100644 --- a/guides/views.md +++ b/guides/views.md @@ -180,7 +180,7 @@ The view's job is not only to render HTML templates. Views are about data presen Phoenix uses [Jason](https://github.com/michalmuskala/jason) to encode JSON, so all we need to do in our views is format the data we'd like to respond with as a list or a map, and Phoenix will do the rest. -While it is possible to respond with JSON back directly from the controller and skip the view, Phoenix Views provide a much more strucgtured approach for doing so. Let's take our `PageController`, and see what it might look like when we respond with some static page maps as JSON, instead of HTML. +While it is possible to respond with JSON back directly from the controller and skip the view, Phoenix Views provide a much more structured approach for doing so. Let's take our `PageController`, and see what it might look like when we respond with some static page maps as JSON, instead of HTML. ```elixir defmodule HelloWeb.PageController do From ffa6bc0db6f1d80ace2301aaf642d633267b300a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 2 Jul 2020 12:50:07 +0200 Subject: [PATCH 087/199] Add missing parens to avoid warnings --- lib/phoenix/endpoint/supervisor.ex | 6 +++--- lib/phoenix/router/helpers.ex | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex index e017a1868b..38f5bae784 100644 --- a/lib/phoenix/endpoint/supervisor.ex +++ b/lib/phoenix/endpoint/supervisor.ex @@ -424,9 +424,9 @@ defmodule Phoenix.Endpoint.Supervisor do end defp warmup_url(endpoint) do - endpoint.url - endpoint.static_url - endpoint.struct_url + endpoint.url() + endpoint.static_url() + endpoint.struct_url() end defp warmup_static(endpoint) do diff --git a/lib/phoenix/router/helpers.ex b/lib/phoenix/router/helpers.ex index 1c65accb65..d0cc374a0c 100644 --- a/lib/phoenix/router/helpers.ex +++ b/lib/phoenix/router/helpers.ex @@ -247,7 +247,7 @@ defmodule Phoenix.Router.Helpers do end def static_url(endpoint, path) when is_atom(endpoint) do - endpoint.static_url <> endpoint.static_path(path) + endpoint.static_url() <> endpoint.static_path(path) end @doc """ From 817a44222908357767559ce8c250c01158638362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 7 Jul 2020 17:24:56 +0200 Subject: [PATCH 088/199] Distillery -> mix release --- guides/deployment/heroku.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/deployment/heroku.md b/guides/deployment/heroku.md index b231fdf20f..551d56ed45 100644 --- a/guides/deployment/heroku.md +++ b/guides/deployment/heroku.md @@ -26,7 +26,7 @@ Heroku is a great platform and Elixir performs well on it. However, you may run If you are just getting started or you don't expect to use the features above, Heroku should be enough for your needs. For instance, if you are migrating an existing application running on Heroku to Phoenix, keeping a similar set of features, Elixir will perform just as well or even better than your current stack. -If you want a platform-as-a-service without these limitations, try [Gigalixir](gigalixir.html). If you would rather deploy to a cloud platform, such as EC2, Google Cloud, etc, consider [Distillery](https://github.com/bitwalker/distillery). +If you want a platform-as-a-service without these limitations, try [Gigalixir](gigalixir.html). If you would rather deploy to a cloud platform, such as EC2, Google Cloud, etc, consider using `mix release`. ## Steps From 7cebf2753d9c4d499a80052367fafb9272ec74ac Mon Sep 17 00:00:00 2001 From: Joe <191664+the-undefined@users.noreply.github.com> Date: Fri, 10 Jul 2020 11:27:00 +0100 Subject: [PATCH 089/199] Fix broken link to contexts guide (#3925) The contexts guide link and name is pluralized where this page was linking to a singular version that doesn't exist. --- guides/testing/testing_contexts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/testing/testing_contexts.md b/guides/testing/testing_contexts.md index 27da083236..48eaffe507 100644 --- a/guides/testing/testing_contexts.md +++ b/guides/testing/testing_contexts.md @@ -4,7 +4,7 @@ > **Requirement**: This guide expects that you have gone through [the Introduction to Testing guide](testing.html). -> **Requirement**: This guide expects that you have gone through [the Context guide](context.html). +> **Requirement**: This guide expects that you have gone through [the Contexts guide](contexts.html). At the end of the Introduction to Testing guide, we generated an HTML resource for posts using the following command: From 9b0b9625679ddc72389f4b1a8ef3232847c0c815 Mon Sep 17 00:00:00 2001 From: batate Date: Mon, 20 Jul 2020 11:43:31 -0400 Subject: [PATCH 090/199] Update community.md to include two Groxio courses (#3934) We released two Phoenix LiveView products that we've been working on for 3 months on Wednesday. --- guides/introduction/community.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/guides/introduction/community.md b/guides/introduction/community.md index 7c01a26983..d38efc9dcd 100644 --- a/guides/introduction/community.md +++ b/guides/introduction/community.md @@ -49,3 +49,8 @@ There are a number of places to connect with community members at all experience * [Build Real-Time Web Apps with Phoenix (Oct 2016)](https://pragprog.com/screencast/v-bhphnx/build-real-time-web-apps-with-phoenix) * [LearnPhoenix.io: Build Scalable, Real-Time Apps with Phoenix, React, and React Native (Aug 2016)](https://www.learnphoenix.io/) + + * [Groxio LiveView: Self Study Program. 20 videos plus a book. (Bruce Tate: July 2020)](https://grox.io/language/liveview/course) + + * [QuadBlock: Tetris-inspired video series teaching layered LiveView design concepts. 16 videos. (Bruce Tate: June 2020)] +(https://grox.io/series/quad) From fd716a2a788f4141bc047cbddee9c3b87fc574c7 Mon Sep 17 00:00:00 2001 From: edjroot Date: Mon, 20 Jul 2020 12:41:30 -0300 Subject: [PATCH 091/199] Grammar: "Peace of mind" is not hyphenated (#3862) --- installer/templates/phx_web/templates/page/index.html.eex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installer/templates/phx_web/templates/page/index.html.eex b/installer/templates/phx_web/templates/page/index.html.eex index b6a19b5a9b..172e21ce59 100644 --- a/installer/templates/phx_web/templates/page/index.html.eex +++ b/installer/templates/phx_web/templates/page/index.html.eex @@ -1,7 +1,7 @@
<%= if gettext do %>

<%%= gettext "Welcome to %{name}!", name: "Phoenix" %>

<% else %>

Welcome to Phoenix!

<% end %> -

Peace-of-mind from prototype to production

<%= if live do %> +

Peace of mind from prototype to production

<%= if live do %>
From 62f6c4c7c369936d9fb0a42e6298236bfcadfa66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 18 Jul 2020 08:27:19 +0200 Subject: [PATCH 092/199] Make docs deps consistent --- mix.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mix.exs b/mix.exs index fc7739c5e9..2234a8184b 100644 --- a/mix.exs +++ b/mix.exs @@ -78,9 +78,9 @@ defmodule Phoenix.MixProject do {:ex_doc, "~> 0.22", only: :docs}, {:inch_ex, "~> 0.2", only: :docs}, - # Test dependencies - {:gettext, "~> 0.15.0", only: :test}, - {:telemetry_poller, "~> 0.4", only: :test}, + # Test dependencies (some also include :docs for cross references) + {:gettext, "~> 0.15.0", only: [:docs, :test]}, + {:telemetry_poller, "~> 0.4", only: [:docs, :test]}, {:telemetry_metrics, "~> 0.4", only: [:docs, :test]}, {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test} ] From 4d6d57cc7d3600b3488f1f37885c009030a4bbe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 17 Jul 2020 19:27:40 +0200 Subject: [PATCH 093/199] Improve error message, closes #3933 --- lib/mix/tasks/phx.gen.cert.ex | 2 +- lib/mix/tasks/phx.gen.channel.ex | 2 +- lib/mix/tasks/phx.gen.context.ex | 2 +- lib/mix/tasks/phx.gen.embedded.ex | 2 +- lib/mix/tasks/phx.gen.html.ex | 2 +- lib/mix/tasks/phx.gen.json.ex | 2 +- lib/mix/tasks/phx.gen.live.ex | 2 +- lib/mix/tasks/phx.gen.presence.ex | 2 +- lib/mix/tasks/phx.gen.schema.ex | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/mix/tasks/phx.gen.cert.ex b/lib/mix/tasks/phx.gen.cert.ex index 34297e1d7d..bb3e02083c 100644 --- a/lib/mix/tasks/phx.gen.cert.ex +++ b/lib/mix/tasks/phx.gen.cert.ex @@ -49,7 +49,7 @@ defmodule Mix.Tasks.Phx.Gen.Cert do @doc false def run(all_args) do if Mix.Project.umbrella?() do - Mix.raise("mix phx.gen.cert can only be run inside an application directory") + Mix.raise("mix phx.gen.cert must be invoked from within your *_web application root directory") end {opts, args} = diff --git a/lib/mix/tasks/phx.gen.channel.ex b/lib/mix/tasks/phx.gen.channel.ex index 420b137749..55079ea088 100644 --- a/lib/mix/tasks/phx.gen.channel.ex +++ b/lib/mix/tasks/phx.gen.channel.ex @@ -26,7 +26,7 @@ defmodule Mix.Tasks.Phx.Gen.Channel do @doc false def run(args) do if Mix.Project.umbrella?() do - Mix.raise "mix phx.gen.channel can only be run inside an application directory" + Mix.raise "mix phx.gen.channel must be invoked from within your *_web application root directory" end [channel_name] = validate_args!(args) context_app = Mix.Phoenix.context_app() diff --git a/lib/mix/tasks/phx.gen.context.ex b/lib/mix/tasks/phx.gen.context.ex index 9f57e2cd77..57cd3c7ec6 100644 --- a/lib/mix/tasks/phx.gen.context.ex +++ b/lib/mix/tasks/phx.gen.context.ex @@ -78,7 +78,7 @@ defmodule Mix.Tasks.Phx.Gen.Context do @doc false def run(args) do if Mix.Project.umbrella?() do - Mix.raise "mix phx.gen.context can only be run inside an application directory" + Mix.raise "mix phx.gen.context must be invoked from within your *_web application root directory" end {context, schema} = build(args) diff --git a/lib/mix/tasks/phx.gen.embedded.ex b/lib/mix/tasks/phx.gen.embedded.ex index 42c7424141..446e8baf8a 100644 --- a/lib/mix/tasks/phx.gen.embedded.ex +++ b/lib/mix/tasks/phx.gen.embedded.ex @@ -34,7 +34,7 @@ defmodule Mix.Tasks.Phx.Gen.Embedded do @doc false def run(args) do if Mix.Project.umbrella?() do - Mix.raise "mix phx.gen.embedded can only be run inside an application directory" + Mix.raise "mix phx.gen.embedded must be invoked from within your *_web application root directory" end schema = build(args) diff --git a/lib/mix/tasks/phx.gen.html.ex b/lib/mix/tasks/phx.gen.html.ex index 8b41e1a048..1619d97e21 100644 --- a/lib/mix/tasks/phx.gen.html.ex +++ b/lib/mix/tasks/phx.gen.html.ex @@ -81,7 +81,7 @@ defmodule Mix.Tasks.Phx.Gen.Html do @doc false def run(args) do if Mix.Project.umbrella?() do - Mix.raise "mix phx.gen.html can only be run inside an application directory" + Mix.raise "mix phx.gen.html must be invoked from within your *_web application root directory" end {context, schema} = Gen.Context.build(args) diff --git a/lib/mix/tasks/phx.gen.json.ex b/lib/mix/tasks/phx.gen.json.ex index 15c89ee3f7..db6bfe6172 100644 --- a/lib/mix/tasks/phx.gen.json.ex +++ b/lib/mix/tasks/phx.gen.json.ex @@ -81,7 +81,7 @@ defmodule Mix.Tasks.Phx.Gen.Json do @doc false def run(args) do if Mix.Project.umbrella?() do - Mix.raise "mix phx.gen.json can only be run inside an application directory" + Mix.raise "mix phx.gen.json must be invoked from within your *_web application root directory" end {context, schema} = Gen.Context.build(args) diff --git a/lib/mix/tasks/phx.gen.live.ex b/lib/mix/tasks/phx.gen.live.ex index f6e71c0940..c7c3788fd5 100644 --- a/lib/mix/tasks/phx.gen.live.ex +++ b/lib/mix/tasks/phx.gen.live.ex @@ -89,7 +89,7 @@ defmodule Mix.Tasks.Phx.Gen.Live do @doc false def run(args) do if Mix.Project.umbrella?() do - Mix.raise "mix phx.gen.live can only be run inside an application directory" + Mix.raise "mix phx.gen.live must be invoked from within your *_web application root directory" end {context, schema} = Gen.Context.build(args) diff --git a/lib/mix/tasks/phx.gen.presence.ex b/lib/mix/tasks/phx.gen.presence.ex index b1ef32314f..4dbb6d3277 100644 --- a/lib/mix/tasks/phx.gen.presence.ex +++ b/lib/mix/tasks/phx.gen.presence.ex @@ -21,7 +21,7 @@ defmodule Mix.Tasks.Phx.Gen.Presence do end def run([alias_name]) do if Mix.Project.umbrella?() do - Mix.raise "mix phx.gen.presence can only be run inside an application directory" + Mix.raise "mix phx.gen.presence must be invoked from within your *_web application root directory" end context_app = Mix.Phoenix.context_app() otp_app = Mix.Phoenix.otp_app() diff --git a/lib/mix/tasks/phx.gen.schema.ex b/lib/mix/tasks/phx.gen.schema.ex index fc87d5761d..d4c7f2f745 100644 --- a/lib/mix/tasks/phx.gen.schema.ex +++ b/lib/mix/tasks/phx.gen.schema.ex @@ -100,7 +100,7 @@ defmodule Mix.Tasks.Phx.Gen.Schema do @doc false def run(args) do if Mix.Project.umbrella?() do - Mix.raise "mix phx.gen.schema can only be run inside an application directory" + Mix.raise "mix phx.gen.schema must be invoked from within your *_web application root directory" end schema = build(args, []) From 133c1ec29a67a0671669a6e2df957b669cf6abfa Mon Sep 17 00:00:00 2001 From: Andrew Haust Date: Sun, 12 Jul 2020 12:20:32 -0400 Subject: [PATCH 094/199] Keep example and clarification consistent (#3929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The example code uses `#{messenger}` while the clarifying explanation uses `#{id}`. Perhaps this was on purpose—and if so, please carry on—but it made me do a few double takes trying to figure out where `#{id}` was referenced in the example. --- guides/controllers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/controllers.md b/guides/controllers.md index 7305746419..59085b37ac 100644 --- a/guides/controllers.md +++ b/guides/controllers.md @@ -125,7 +125,7 @@ def show(conn, %{"messenger" => messenger}) do end ``` -Hitting `/hello/Frank` now renders the HTML string we defined in the `show` action. Note that what we wrote in the action is not an `eex` template. It's a multi-line string, so we interpolate the `id` variable like this `#{id}`, instead of this `<%= id %>`. +Hitting `/hello/Frank` now renders the HTML string we defined in the `show` action. Note that what we wrote in the action is not an `eex` template. It's a multi-line string, so we interpolate the `messenger` variable like this `#{messenger}`, instead of this `<%= messenger %>`. It is worth noting that the `text/2`, `json/2`, and `html/2` functions require neither a Phoenix view, nor a template to render. From d66738cd4898778cdcfe0180f84b6c87ce0cd9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 13 Jul 2020 21:34:28 +0200 Subject: [PATCH 095/199] Convert deployment options to bullet list --- guides/deployment/deployment.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/guides/deployment/deployment.md b/guides/deployment/deployment.md index 51a881f513..64a0f77d65 100644 --- a/guides/deployment/deployment.md +++ b/guides/deployment/deployment.md @@ -112,4 +112,8 @@ $ MIX_ENV=prod mix ecto.migrate $ PORT=4001 MIX_ENV=prod mix phx.server ``` -And that's it. Next you can learn how to deploy [with Elixir's releases](releases.html), [to Gigalixir](gigalixir.html), and [to Heroku](heroku.html). +And that's it. Next you can use one of our official guides to deploy: + + * [with Elixir's releases](releases.html) + * [to Gigalixir](gigalixir.html), an Elixir-centric Platform as a Service (PaaS) + * and [to Heroku](heroku.html), one of the most popular PaaS From 24e1301dbfd603538990cfa43ad456f10ebe01b7 Mon Sep 17 00:00:00 2001 From: Andrew Haust Date: Sun, 12 Jul 2020 12:19:42 -0400 Subject: [PATCH 096/199] Fix typo in guides (#3930) --- guides/views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/views.md b/guides/views.md index ac2ee4e126..b92ed073d5 100644 --- a/guides/views.md +++ b/guides/views.md @@ -107,7 +107,7 @@ Pretty neat, right? At compile-time, Phoenix precompiles all `*.html.eex` templa ### Manually rendering templates -So far, Phoenix has taken care of putting every in place and rendering views for us. However, we can also render views directly. +So far, Phoenix has taken care of putting everything in place and rendering views for us. However, we can also render views directly. Let's create a new template to play around with, `lib/hello_web/templates/page/test.html.eex`: From bb88c55cd8e62a797abcba2e44617f66f0716cc8 Mon Sep 17 00:00:00 2001 From: Aaron Renner Date: Sun, 12 Jul 2020 10:20:13 -0600 Subject: [PATCH 097/199] Add additional example of using tag_values in Telemetry guide (#3928) --- guides/telemetry.md | 55 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/guides/telemetry.md b/guides/telemetry.md index e5027ac626..59e97d825a 100644 --- a/guides/telemetry.md +++ b/guides/telemetry.md @@ -397,6 +397,61 @@ route. Note the `:tags` and `:tag_values` options can be applied to all `Telemetry.Metrics` types. +### Renaming value labels using tag values + +Sometimes when displaying a metric, the value label may need to be transformed +to improve readability. Take for example the following metric that displays the +duration of the each LiveView's `mount/3` callback by `connected?` status. + +```elixir +summary("phoenix.live_view.mount.stop.duration", + unit: {:native, :millisecond}, + tags: [:view, :connected?], + tag_values: &live_view_metric_tag_values/1 +) +``` + +The following function lifts `metadata.socket.view` and +`metadata.socket.connected?` to be top-level keys on `metadata`, as we did in +the previous example. + +```elixir +# lib/my_app_web/telemetry.ex +defp live_view_metric_tag_values(metadata) do + metadata + |> Map.put(:view, metadata.socket.view) + |> Map.put(:connected?, metadata.socket.connected?) +end +``` + +However, when rendering these metrics in LiveDashboard, the value label is +output as `"Elixir.Phoenix.LiveDashboard.MetricsLive true"`. + +To make the value label easier to read, we can update our private function to +generate more user friendly names. We'll run the value of the `:view` through +`inspect/1` to remove the `Elixir.` prefix and call another private function to +convert the `connected?` boolean into human readable text. + +```elixir +# lib/my_app_web/telemetry.ex +defp live_view_metric_tag_values(metadata) do + metadata + |> Map.put(:view, inspect(metadata.socket.view)) + |> Map.put(:connected?, get_connection_status(metadata.socket)) +end + +defp get_connection_status(%{connected?: true}), do: "Connected" +defp get_connection_status(%{connected?: false}), do: "Disconnected" +``` + +Now the value label will be rendered like `"Phoenix.LiveDashboard.MetricsLive +Connected"`. + +Hopefully, this gives you some inspiration on how to use the `:tag_values` +option. Just remember to keep this function fast since it is called on every +event. + + ## Periodic measurements You might want to periodically measure key values within From 33d9f398371db45a38e5060b5fe3a3a3ed0ceb29 Mon Sep 17 00:00:00 2001 From: AJ Foster Date: Fri, 10 Jul 2020 06:38:29 -0400 Subject: [PATCH 098/199] Correct typespecs for delegated get_flash/{1,2} (#3926) --- lib/phoenix/test/conn_test.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/phoenix/test/conn_test.ex b/lib/phoenix/test/conn_test.ex index 64779cce96..7b06ed8b18 100644 --- a/lib/phoenix/test/conn_test.ex +++ b/lib/phoenix/test/conn_test.ex @@ -273,13 +273,13 @@ defmodule Phoenix.ConnTest do @doc """ Gets the whole flash storage. """ - @spec get_flash(Conn.t) :: Conn.t + @spec get_flash(Conn.t) :: map defdelegate get_flash(conn), to: Phoenix.Controller @doc """ Gets the given key from the flash storage. """ - @spec get_flash(Conn.t, term) :: Conn.t + @spec get_flash(Conn.t, term) :: term defdelegate get_flash(conn, key), to: Phoenix.Controller @doc """ From 2d38dd6ef8a148baa4d6cec0095fa8b054c72a5e Mon Sep 17 00:00:00 2001 From: James Robey Date: Tue, 7 Jul 2020 22:41:45 +0700 Subject: [PATCH 099/199] bug fix: mix phx.gen.json --web namespace should include alias in the suggested routing (#3916) --- lib/mix/tasks/phx.gen.json.ex | 2 +- test/mix/tasks/phx.gen.json_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/tasks/phx.gen.json.ex b/lib/mix/tasks/phx.gen.json.ex index db6bfe6172..93a65daf04 100644 --- a/lib/mix/tasks/phx.gen.json.ex +++ b/lib/mix/tasks/phx.gen.json.ex @@ -141,7 +141,7 @@ defmodule Mix.Tasks.Phx.Gen.Json do Add the resource to your #{schema.web_namespace} :api scope in #{Mix.Phoenix.web_path(ctx_app)}/router.ex: - scope "/#{schema.web_path}", #{inspect Module.concat(context.web_module, schema.web_namespace)} do + scope "/#{schema.web_path}", #{inspect Module.concat(context.web_module, schema.web_namespace)}, as: :#{schema.web_path} do pipe_through :api ... resources "/#{schema.plural}", #{inspect schema.alias}Controller diff --git a/test/mix/tasks/phx.gen.json_test.exs b/test/mix/tasks/phx.gen.json_test.exs index f3fe7cd897..39dec3351e 100644 --- a/test/mix/tasks/phx.gen.json_test.exs +++ b/test/mix/tasks/phx.gen.json_test.exs @@ -131,7 +131,7 @@ defmodule Mix.Tasks.Phx.Gen.JsonTest do Add the resource to your Blog :api scope in lib/phoenix_web/router.ex: - scope "/blog", PhoenixWeb.Blog do + scope "/blog", PhoenixWeb.Blog, as: :blog do pipe_through :api ... resources "/posts", PostController From 19e3e5a5d44becb49c32ea3c195157d53d8fc440 Mon Sep 17 00:00:00 2001 From: Pavel Shpak Date: Tue, 7 Jul 2020 18:40:50 +0300 Subject: [PATCH 100/199] Fix plugs as composition example (#3908) - Eliminate plug methods dependency from the first example - Reuse data already found, like message It makes more clear and fair comparison. But it still shows nesting complexity in the first example and much more composable, clear, and reusable way in the second example. --- guides/plug.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/guides/plug.md b/guides/plug.md index d044fc59f3..d18c88bffe 100644 --- a/guides/plug.md +++ b/guides/plug.md @@ -228,17 +228,16 @@ defmodule HelloWeb.MessageController do use HelloWeb, :controller def show(conn, params) do - case authenticate(conn) do + case Authenticator.find_user(conn) do {:ok, user} -> case find_message(params["id"]) do nil -> conn |> put_flash(:info, "That message wasn't found") |> redirect(to: "/") message -> - case authorize_message(conn, params["id"]) do - :ok -> - render(conn, :show, page: find_message(params["id"])) - :error -> - conn |> put_flash(:info, "You can't access that page") |> redirect(to: "/") + if Authorizer.can_access?(user, message) do + render(conn, :show, page: message) + else + conn |> put_flash(:info, "You can't access that page") |> redirect(to: "/") end end :error -> @@ -259,7 +258,7 @@ defmodule HelloWeb.MessageController do plug :authorize_message def show(conn, params) do - render(conn, :show, page: find_message(params["id"])) + render(conn, :show, page: conn.assigns[:message]) end defp authenticate(conn, _) do From 464192b4dbaa2d7d24980b8792c0da7c978a295f Mon Sep 17 00:00:00 2001 From: Pavel Shpak Date: Tue, 7 Jul 2020 18:39:33 +0300 Subject: [PATCH 101/199] Fix some missed pipeline usage in admin scope cases (#3909) --- guides/routing.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/guides/routing.md b/guides/routing.md index b418489a93..bea13811e4 100644 --- a/guides/routing.md +++ b/guides/routing.md @@ -318,6 +318,8 @@ scope "/", HelloWeb do end scope "/admin", HelloWeb.Admin do + pipe_through :browser + resources "/reviews", ReviewController end ``` @@ -352,6 +354,8 @@ We can fix this problem by adding an `as: :admin` option to our admin scope: ```elixir scope "/admin", HelloWeb.Admin, as: :admin do + pipe_through :browser + resources "/reviews", ReviewController end ``` From 8ddc18a66cac92978ed3c69c6ceef98a683b1849 Mon Sep 17 00:00:00 2001 From: Pavel Shpak Date: Tue, 7 Jul 2020 18:27:10 +0300 Subject: [PATCH 102/199] Fix migrate task description (#3911) - Remove not existed -v option - Correct description of --to option --- guides/mix_tasks.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/guides/mix_tasks.md b/guides/mix_tasks.md index 4c45d56d8f..323a44eb17 100644 --- a/guides/mix_tasks.md +++ b/guides/mix_tasks.md @@ -585,13 +585,7 @@ The `--step` option will behave the same way. mix ecto.migrate --step 2 ``` -We can also specify an individual migration we would like to run with the `-v` option. - -```console -mix ecto.migrate -v 20150317170448 -``` - -The `--to` option will behave the same way. +The `--to` option will run all migrations up to and including given version. ```console mix ecto.migrate --to 20150317170448 @@ -690,4 +684,4 @@ If you want to make your new mix task to use your application's infrastructure, Mix.shell().info("Now I have access to Repo and other goodies!") end . . . -``` \ No newline at end of file +``` From f6170ebe0f63b8c14986d138062da162692c393c Mon Sep 17 00:00:00 2001 From: Max Veytsman Date: Tue, 7 Jul 2020 11:26:49 -0400 Subject: [PATCH 103/199] Fix bug in presence documentation example (#3913) The example presence code has a bug where it's building a map of users keyed by id, but then using a key that it gets from the presences map to index into it. The users table is going to have integer primary keys, but presences keys are cast to a string (see https://hexdocs.pm/phoenix/Phoenix.Presence.html#c:track/3) This means that the code as written will always return `nil` for the user. I'm not sure if adding `String.to_integer` here is the cleanest approach, please let me know if there's a better a way to do this! --- lib/phoenix/presence.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix/presence.ex b/lib/phoenix/presence.ex index 155499d2ce..25a00a306e 100644 --- a/lib/phoenix/presence.ex +++ b/lib/phoenix/presence.ex @@ -100,7 +100,7 @@ defmodule Phoenix.Presence do users = presences |> Map.keys() |> Accounts.get_users_map() for {key, %{metas: metas}} <- presences, into: %{} do - {key, %{metas: metas, user: users[key]}} + {key, %{metas: metas, user: users[String.to_integer(key)]}} end end From 4003644d09cf3e773416078889685db1be20cc89 Mon Sep 17 00:00:00 2001 From: Elaine Naomi Date: Tue, 7 Jul 2020 12:24:10 -0300 Subject: [PATCH 104/199] Remove broken link on community page (#3915) --- guides/introduction/community.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/guides/introduction/community.md b/guides/introduction/community.md index d38efc9dcd..490d88cdbf 100644 --- a/guides/introduction/community.md +++ b/guides/introduction/community.md @@ -46,8 +46,6 @@ There are a number of places to connect with community members at all experience * [LearnPhoenix.tv: Learn how to Build Fast, Dependable Web Apps with Phoenix (Feb 2017)](https://www.learnphoenix.tv/) - * [Build Real-Time Web Apps with Phoenix (Oct 2016)](https://pragprog.com/screencast/v-bhphnx/build-real-time-web-apps-with-phoenix) - * [LearnPhoenix.io: Build Scalable, Real-Time Apps with Phoenix, React, and React Native (Aug 2016)](https://www.learnphoenix.io/) * [Groxio LiveView: Self Study Program. 20 videos plus a book. (Bruce Tate: July 2020)](https://grox.io/language/liveview/course) From 02c3778059c2137e2e28366e6e8f2de555ff5fb0 Mon Sep 17 00:00:00 2001 From: Henrik Nyh Date: Tue, 7 Jul 2020 16:21:52 +0100 Subject: [PATCH 105/199] contexts.md: Update migration example output (#3919) To go with the change in https://github.com/elixir-ecto/ecto_sql/pull/240. --- guides/contexts.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/contexts.md b/guides/contexts.md index 5baf7d17f1..fab91ca117 100644 --- a/guides/contexts.md +++ b/guides/contexts.md @@ -45,7 +45,7 @@ Generated hello app The database for Hello.Repo has been dropped The database for Hello.Repo has been created -14:38:37.418 [info] Already up +14:38:37.418 [info] Migrations already up ``` Now we're ready to create our accounts context. We'll use `mix phx.gen.html` which creates a context module that wraps up Ecto access for creating, updating, and deleting users, along with web files like controllers and templates for the web interface into our context. Run the following command at your project root: From 24de756d08ce4849c359582ad1747288a07ac5da Mon Sep 17 00:00:00 2001 From: Ali Raheem Date: Tue, 7 Jul 2020 16:16:57 +0100 Subject: [PATCH 106/199] Example uses render_to_string not render_to_iodata (#3921) --- guides/views.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/views.md b/guides/views.md index b92ed073d5..2fca3cd150 100644 --- a/guides/views.md +++ b/guides/views.md @@ -131,7 +131,7 @@ iex(2)> Phoenix.View.render(HelloWeb.PageView, "test.html", message: "