diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c11117b32a..6685cb92c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,8 +22,11 @@ jobs: - elixir: 1.14.5 otp: 25.3.2.9 + - elixir: 1.15.x + otp: 25.x + - elixir: 1.17.3 - otp: 27.1 + otp: 27.2 lint: true installer: true diff --git a/.gitignore b/.gitignore index 44fba26034..e46730c560 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ /installer/deps/ /installer/doc/ /installer/phx_new-*.ez +/installer/tmp/ /integration_test/_build/ /integration_test/deps/ diff --git a/CHANGELOG.md b/CHANGELOG.md index d3849ce1c1..2cc0b330c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,241 +1,5 @@ -# Changelog for v1.7 +# Changelog for v1.8 -See the [upgrade guide](https://gist.github.com/chrismccord/00a6ea2a96bc57df0cce526bd20af8a7) to upgrade from Phoenix 1.6.x. +## v1.7 -Phoenix v1.7 requires Elixir v1.11+ & Erlang v22.1+. - -## Introduction of Verified Routes - -Phoenix 1.7 includes a new `Phoenix.VerifiedRoutes` feature which provides `~p` -for route generation with compile-time verification. - -Use of the `sigil_p` macro allows paths and URLs throughout your -application to be compile-time verified against your Phoenix router(s). -For example the following path and URL usages: - - <.link href={~p"/sessions/new"} method="post">Log in - - redirect(to: url(~p"/posts/#{post}")) - -Will be verified against your standard `Phoenix.Router` definitions: - - get "/posts/:post_id", PostController, :show - post "/sessions/new", SessionController, :create - -Unmatched routes will issue compiler warnings: - - warning: no route path for AppWeb.Router matches "/postz/#{post}" - lib/app_web/controllers/post_controller.ex:100: AppWeb.PostController.show/2 - -*Note: Elixir v1.14+ is required for comprehensive warnings. Older versions -will work properly and warn on new compilations, but changes to the router file -will not issue new warnings.* - -This feature replaces the `Helpers` module generated in your Phoenix router, but helpers -will continue to work and be generated. You can disable router helpers by passing the -`helpers: false` option to `use Phoenix.Router`. - -## phx.new revamp - -The `phx.new` application generator has been improved to rely on function components for -both Controller and LiveView rendering, ultimately simplifying the rendering stack of -Phoenix applications and providing better reuse. - -New applications come with a collection of well-documented and accessible core components, -styled with Tailwind CSS by default. You can opt-out of Tailwind CSS with the `--no-tailwind` -flag (the Tailwind CSS classes are kept in the generated components as reference for -future styling). - -## 1.7.14 (2024-06-18) - -### Bug fixes - * Revert "Add `follow_redirect/2` to Phoenix.ConnTest" (#5797) as this conflicts with `follow_redirect/2` in LiveView, which is imported with ConnTest by default - -## 1.7.13 (2024-06-18) - -### Bug fixes - * Fix Elixir 1.17 warning in Cowboy2Adapter - * Fix verified routes emitting diagnostics without file and position - -### JavaScript Client Bug Fixes - * Fix error when `sessionStorage` is not available on global namespace - -### Enhancements - * Add `follow_redirect/2` to Phoenix.ConnTest - * Use LiveView 1.0.0-rc for newly generated applications - * Use new `Phoenix.Component.used_input?` for form errors in generated `core_components.ex` - * Allow `mix ecto.setup` from the umbrella root - * Bump Endpoint static cache manifest on `config_change` callback - -## 1.7.12 (2024-04-11) - -### JavaScript Client Bug Fixes - * Fix all unjoined channels from being removed from the socket when channel leave is called on any single unjoined channel instance - -### Enhancements - * [phx.gen.auth] Add enhanced session fixation protection. - For applications whichs previously used `phx.gen.auth`, the following line can be added to the `renew_session` function in the auth module: - - ```diff - defp renew_session(conn) do - + delete_csrf_token() - - conn - |> configure_session(renew: true) - |> clear_session() - ``` - - *Note*: because the session id is in a http-only cookie by default, the only way to perform this attack prior to this change is if your application was already vulnerable to an XSS attack, which itself grants more escalated "privileges” than the CSRF fixation. - -### JavaScript Client Enhancements - * Only memorize longpoll fallback for browser session if WebSocket never had a successful connection - -## 1.7.11 (2024-02-01) - -### Enhancements - * [phx.new] Default to the [Bandit webserver](https://github.com/mtrudel/bandit) for newly generated applications - * [phx.new] Enable longpoll transport by default and auto fallback when websocket fails for newly generated applications - -### JavaScript Client Enhancements - * Support new `longPollFallbackMs` option to auto fallback when websocket fails to connect - * Support new `debug` option to enable verbose logging - -### Deprecations - * Deprecate the `c:init/2` callback in endpoints in favor of `config/runtime.exs` or in favor of `{Phoenix.Endpoint, options}` - -## 1.7.10 (2023-11-03) - -### Bug fixes - * [phx.new] Fix `CoreComponents.flash` generating incorrect id's causing flash messages to fail to be closed when clicked - -### Enhancements - * [Phoenix.Endpoint] Support dynamic port for `Endpoint.url/0` - -## 1.7.9 (2023-10-11) - -### Bug fixes - * [Phoenix.CodeReloader] - Fix error in code reloader causing compilation errors - * [phx.new] – fix LiveView debug heex configuration being generated when `--no-html` pas passed - -## 1.7.8 (2023-10-09) - -### Bug fixes - * [Phoenix.ChannelTest] Stringify lists when pushing data - * [Phoenix.Controller] Fix filename when sending downloads with non-ascii names - * [Phoenix.CodeReloader] Remove duplicate warnings on recent Elixir versions - * [Phoenix.CodeReloader] Do not crash code reloader if file information is missing from diagnostic - * [Phoenix.Logger] Do not crash when status is atom - * [phx.gen.release] Fix `mix phx.gen.release --docker` failing with `:http_util` error on Elixir v1.15 - * [phx.gen.*] Skip map inputs in generated forms as there is no trivial matching input - * [phx.new] Fix tailwind/esbuild config and paths in umbrella projects - * [phx.new] Do not render `th` for actions if actions are empty - -### Enhancements - * [Phoenix] Allow latest `plug_crypto` - * [Phoenix.Endpoint] Support dynamic socket drainer configuration - * [Phoenix.Logger] Change socket serializer/version logs to warning - * [Phoenix.VerifiedRoutes] Add support for static resources with fragments in `~p` - * [phx.gen.schema] Support `--repo` and `--migration-dir` flags - * [phx.new] Allow `<.input type="checkbox">` without `value` attr in core components - * [phx.new] Allow UTC datetimes in the generators - * [phx.new] Automatically migrate when release starts when using sqlite 3 - * [phx.new] Allow ID to be assigned in flash component - * [phx.new] Add `--adapter` flag for generating application with bandit - * [phx.new] Include DNSCluster for simple clustering - * [phx.routes] Support `--method` option - -## 1.7.7 (2023-07-10) - -### Enhancements - * Support incoming binary payloads to channels over longpoll transport - -## 1.7.6 (2023-06-16) - -### Bug Fixes - * Support websock_adapter 0.5.3 - -### Enhancements - * Allow using Phoenix.ChannelTest socket/connect in another process - -## 1.7.5 (2023-06-15) - -### Bug Fixes - * Fix LongPoll error when draining connections - -## 1.7.4 (2023-06-15) - -### Bug Fixes - * Fix the WebSocket draining sending incorrect close code when draining causing LiveViews to reload the page instead of reconnecting - -## 1.7.3 (2023-05-30) - -### Enhancements - * Use LiveView 0.19 for new apps - -### Bug Fixes - * Fix compilation error page on plug debugger showing obscure error when app fails to compile - * Fix warnings being printed twice in route verification - -## 1.7.2 (2023-03-20) - -### Enhancements - * [Endpoint] Add socket draining for batched and orchestrated Channel/LiveView socket shutdown - * [code reloader] Improve the compilation error page to remove horizontal scrolling and include all warnings and errors from compilation - * [phx.new] Support the `--no-tailwind` and `--no-esbuild` flags - * [phx.new] Move heroicons to assets/vendor - * [phx.new] Simplify core modal to use the new JS.exec instruction to reduce footprint - * [sockets] Allow custom csrf_token_keys in WebSockets - -## 1.7.1 (2023-03-02) - -### Enhancements - * [phx.new] Embed heroicons in app.css bundle to optimize usage - -## 1.7.0 (2023-02-24) - -### Bug Fixes - * Fix race conditions in the longpoll transport by batching messages - -## 1.7.0-rc.3 (2023-02-15) - -### Enhancements - * Use stream based collections for `phx.gen.live` generators - * Update `phx.gen.live` generators to use `Phoenix.Component.to_form` - -## 1.7.0-rc.2 (2023-01-13) - -### Bug Fixes - * [Router] Fix routing bug causing incorrect matching order on similar routes - * [phx.new] Fix installation hanging in some cases - -## 1.7.0-rc.1 (2023-01-06) - -### Enhancements - * Raise if using verified routes outside of functions - * Add tailwind.install/esbuild.install to mix setup - -### Bug Fixes - * [Presence] fix task shutdown match causing occasional presence errors - * [VerifiedRoutes] Fix expansion causing more compile-time deps than necessary - * [phx.gen.auth] Add password inputs to password reset edit form - * [phx.gen.embedded] Fixes missing :references generation to phx.gen.embedded - * Fix textarea rendering in core components - * Halt all sockets on intercept to fix longpoll response already sent error - -## 1.7.0-rc.0 (2022-11-07) - -### Deprecations - * `Phoenix.Controller.get_flash` has been deprecated in favor of the new `Phoenix.Flash` module, which provides unified flash access - -### Enhancements - * [Router] Add `Phoenix.VerifiedRoutes` for `~p`-based route generation with compile-time verification. - * [Router] Support `helpers: false` to `use Phoenix.Router` to disable helper generation - * [Router] Add `--info [url]` switch to `phx.routes` to get route information about a url/path - * [Flash] Add `Phoenix.Flash` for unfied flash access - -### JavaScript Client Bug Fixes - * Fix heartbeat being sent after disconnect and causing abnormal disconnects - -## v1.6 - -The CHANGELOG for v1.6 releases can be found in the [v1.6 branch](https://github.com/phoenixframework/phoenix/blob/v1.6/CHANGELOG.md). +The CHANGELOG for v1.7 releases can be found in the [v1.7 branch](https://github.com/phoenixframework/phoenix/blob/v1.7/CHANGELOG.md). diff --git a/README.md b/README.md index 5374f74e89..ce1791f55e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -![phoenix logo](https://raw.githubusercontent.com/phoenixframework/phoenix/main/priv/static/phoenix.png) + + + + Phoenix logo + > Peace of mind from prototype to production. diff --git a/RELEASE.md b/RELEASE.md index 9a95c77e03..e3fd77c68e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,6 +1,6 @@ # Release Instructions - 1. Check related deps for required version bumps and compatibility (`phoenix_ecto`, `phoenix_pubsub_redis`, `phoenix_html`) + 1. Check related deps for required version bumps and compatibility (`phoenix_ecto`, `phoenix_html`) 2. Bump version in related files below 3. Bump external dependency version in related external files below 4. Run tests: @@ -10,7 +10,8 @@ 6. Publish `phx_new` and `phoenix` packages and docs after pruning any extraneous uncommitted files 7. Test installer by generating a new app, running `mix deps.get`, and compiling 8. Publish to `npm` with `npm publish` - 9. Start -dev version in related files below + 9. Update Elixir and Erlang/OTP versions on new.phoenixframework.org + 10. Start -dev version in related files below ## Files with version @@ -21,5 +22,6 @@ * `assets/package.json` ## Files with external dependency versions + * `priv/templates/phx.gen.release/Docker.eex` (debian) * `priv/templates/phx.gen.release/Docker.eex` (esbuild) diff --git a/guides/components.md b/guides/components.md index 83e6710e8c..5e98f92131 100644 --- a/guides/components.md +++ b/guides/components.md @@ -16,7 +16,7 @@ At the end of the Request life-cycle chapter, we created a template at `lib/hell ```heex
-

Hello World, from <%= @messenger %>!

+

Hello World, from {@messenger}!

``` @@ -34,7 +34,7 @@ That's simple enough. There's only two lines, `use HelloWeb, :html`. This line c All of the imports and aliases we make in our module will also be available in our templates. That's because templates are effectively compiled into functions inside their respective module. For example, if you define a function in your module, you will be able to invoke it directly from the template. Let's see this in practice. -Imagine we want to refactor our `show.html.heex` to move the rendering of `

Hello World, from <%= @messenger %>!

` to its own function. We can move it to a function component inside `HelloHTML`, let's do so: +Imagine we want to refactor our `show.html.heex` to move the rendering of `

Hello World, from {@messenger}!

` to its own function. We can move it to a function component inside `HelloHTML`, let's do so: ```elixir defmodule HelloWeb.HelloHTML do @@ -46,7 +46,7 @@ defmodule HelloWeb.HelloHTML do def greet(assigns) do ~H""" -

Hello World, from <%= @messenger %>!

+

Hello World, from {@messenger}!

""" end end @@ -89,19 +89,21 @@ Next, let's fully understand the expressive power behind the HEEx template langu ## HEEx -Function components and templates files are powered by [the HEEx template language](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#sigil_H/2), which stands for "HTML+EEx". EEx is an Elixir library that uses `<%= expression %>` to execute Elixir expressions and interpolate their results into the template. This is frequently used to display assigns we have set by way of the `@` shortcut. In your controller, if you invoke: +Function components and templates files are powered by [the HEEx template language](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#sigil_H/2), which stands for "HTML+EEx". EEx is an Elixir library that uses `<%= expression %>` to execute Elixir expressions and interpolate their results into arbitrary text templates. HEEx extends EEx for writing HTML templates mixed with Elixir interpolation. We can write Elixir code inside `{...}` for HTML-aware interpolation inside tag attributes and the body. We can also interpolate arbitrary HEEx blocks using EEx interpolation (`<%= ... %>`). We use `@name` to access the key `name` defined inside `assigns`. + +This is frequently used to display assigns we have set by way of the `@` shortcut. In your controller, if you invoke: ```elixir render(conn, :show, username: "joe") ``` -Then you can access said username in the templates as `<%= @username %>`. In addition to displaying assigns and functions, we can use pretty much any Elixir expression. For example, in order to have conditionals: +Then you can access said username in the templates as `{@username}`. In addition to displaying assigns and functions, we can use pretty much any Elixir expression. For example, in order to have conditionals: ```heex <%= if some_condition? do %> -

Some condition is true for user: <%= @username %>

+

Some condition is true for user: {@username}

<% else %> -

Some condition is false for user: <%= @username %>

+

Some condition is false for user: {@username}

<% end %> ``` @@ -115,8 +117,8 @@ or even loops: <%= for number <- 1..10 do %> - <%= number %> - <%= number * number %> + {number} + {number * number} <% end %> @@ -131,20 +133,20 @@ HEEx also comes with handy HTML extensions we will learn next. Besides allowing interpolation of Elixir expressions via `<%= %>`, `.heex` templates come with HTML-aware extensions. For example, let's see what happens if you try to interpolate a value with "<" or ">" in it, which would lead to HTML injection: ```heex -<%= "Bold?" %> +{"Bold?"} ``` Once you render the template, you will see the literal `` on the page. This means users cannot inject HTML content on the page. If you want to allow them to do so, you can call `raw`, but do so with extreme care: ```heex -<%= raw "Bold?" %> +{raw("Bold?")} ``` -Another super power of HEEx templates is validation of HTML and lean interpolation syntax of attributes. You can write: +Another super power of HEEx templates is validation of HTML and interpolation syntax of attributes. You can write: ```heex
-

Hello <%= @username %>

+

Hello {@username}

``` @@ -154,7 +156,7 @@ To interpolate a dynamic number of attributes in a keyword list or map, do: ```heex
-

Hello <%= @username %>

+

Hello {@username}

``` @@ -178,7 +180,7 @@ Likewise, for comprehensions may be written as: ```heex ``` @@ -189,7 +191,7 @@ Layouts are just function components. They are defined in a module, just like al You may be wondering how the string resulting from a rendered view ends up inside a layout. That's a great question! If we look at `lib/hello_web/components/layouts/root.html.heex`, just about at the end of the ``, we will see this. ```heex -<%= @inner_content %> +{@inner_content} ``` In other words, after rendering your page, the result is placed in the `@inner_content` assign. diff --git a/guides/contexts.md b/guides/contexts.md index 273b35517c..22fc256eb4 100644 --- a/guides/contexts.md +++ b/guides/contexts.md @@ -73,7 +73,7 @@ Phoenix generated the web files as expected in `lib/hello_web/`. We can also see With the new route in place, Phoenix reminds us to update our repo by running `mix ecto.migrate`, but first we need to make a few tweaks to the generated migration in `priv/repo/migrations/*_create_products.exs`: -```elixir +```diff def change do create table(:products) do add :title, :string @@ -498,8 +498,9 @@ Next, let's expose our new feature to the web by adding the category input to ou |> Ecto.Changeset.get_change(:categories, []) |> Enum.map(& &1.data.id) - for cat <- Hello.Catalog.list_categories(), - do: [key: cat.title, value: cat.id, selected: cat.id in existing_ids] + for cat <- Hello.Catalog.list_categories() do + [key: cat.title, value: cat.id, selected: cat.id in existing_ids] + end end ``` @@ -524,10 +525,9 @@ We added a `category_select` above our save button. Now let's try it out. Next, <.list> ... + <:item title="Categories"> -+ <%= for cat <- @product.categories do %> -+ <%= cat.title %> -+
-+ <% end %> ++ + ``` @@ -605,12 +605,11 @@ Would you like to proceed? [Yn] y Remember to update your repository by running migrations: $ mix ecto.migrate - ``` We generated a new resource inside our `ShoppingCart` named `CartItem`. This schema and table will hold references to a cart and product, along with the price at the time we added the item to our cart, and the quantity the user wishes to purchase. Let's touch up the generated migration file in `priv/repo/migrations/*_create_cart_items.ex`: -```elixir +```diff create table(:cart_items) do - add :price_when_carted, :decimal + add :price_when_carted, :decimal, precision: 15, scale: 6, null: false @@ -664,7 +663,7 @@ Our `Catalog.Product` resource serves to keep the responsibilities of representi Now that we know where our data dependencies exist, let's add our schema associations so we can tie shopping cart items to products. First, let's make a quick change to our cart schema in `lib/hello/shopping_cart/cart.ex` to associate a cart to its items: -```elixir +```diff schema "carts" do field :user_uuid, Ecto.UUID @@ -676,7 +675,7 @@ Now that we know where our data dependencies exist, let's add our schema associa Now that our cart is associated to the items we place in it, let's set up the cart item associations inside `lib/hello/shopping_cart/cart_item.ex`: -```elixir +```diff schema "cart_items" do field :price_when_carted, :decimal field :quantity, :integer @@ -708,7 +707,7 @@ As we mentioned before, the context generators are only a starting point for our We won't focus on a real user authentication system at this point, but by the time we're done, you'll be able to naturally integrate one with what we've written here. To simulate a current user session, open up your `lib/hello_web/router.ex` and key this in: -```elixir +```diff pipeline :browser do plug :accepts, ["html"] plug :fetch_session @@ -797,7 +796,7 @@ We defined a new `CartItemController` with the create and delete actions that we Let's implement the new interface for the `ShoppingCart` context API in `lib/hello/shopping_cart.ex`: -```elixir +```diff + alias Hello.Catalog - alias Hello.ShoppingCart.Cart + alias Hello.ShoppingCart.{Cart, CartItem} @@ -937,34 +936,28 @@ We created a view to render our `show.html` template and aliased our `ShoppingCa Next we can create the template at `lib/hello_web/controllers/cart_html/show.html.heex`: ```heex -<%= if @cart.items == [] do %> - <.header> - My Cart - <:subtitle>Your cart is empty - -<% else %> - <.header> - My Cart - +<.header> + My Cart + <:subtitle :if={@cart.items == []}>Your cart is empty + +
<.simple_form :let={f} for={@changeset} action={~p"/cart"}> - <.inputs_for :let={item_form} field={f[:items]}> - <% item = item_form.data %> + <.inputs_for :let={%{data: item} = item_form} field={f[:items]}> <.input field={item_form[:quantity]} type="number" label={item.product.title} /> - <%= currency_to_str(ShoppingCart.total_item_price(item)) %> + {currency_to_str(ShoppingCart.total_item_price(item))} <:actions> <.button>Update cart - - Total: <%= currency_to_str(ShoppingCart.total_cart_price(@cart)) %> -<% end %> + Total: {currency_to_str(ShoppingCart.total_cart_price(@cart))} +
<.back navigate={~p"/products"}>Back to products ``` -We started by showing the empty cart message if our preloaded `cart.items` is empty. If we have items, we use the `simple_form` component provided by our `HelloWeb.CoreComponents` to take our cart changeset that we assigned in the `CartController.show/2` action and create a form which maps to our cart controller `update/2` action. Within the form, we use the [`inputs_for`](`Phoenix.Component.inputs_for/1`) component to render inputs for the nested cart items. This will allow us to map item inputs back together when the form is submitted. Next, we display a number input for the item quantity and label it with the product title. We finish the item form by converting the item price to string. We haven't written the `ShoppingCart.total_item_price/1` function yet, but again we employed the idea of clear, descriptive public interfaces for our contexts. After rendering inputs for all the cart items, we show an "update cart" submit button, along with the total price of the entire cart. This is accomplished with another new `ShoppingCart.total_cart_price/1` function which we'll implement in a moment. Finally, we added a `back` component to go back to our products page. +We started by showing the empty cart message if our preloaded `cart.items` is empty. If we have items, we use the `simple_form` component provided by our `HelloWeb.CoreComponents` to take our cart changeset that we assigned in the `CartController.show/2` action and create a form which maps to our cart controller `update/2` action. Within the form, we use the [`inputs_for`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#inputs_for/1) component to render inputs for the nested cart items. This will allow us to map item inputs back together when the form is submitted. Next, we display a number input for the item quantity and label it with the product title. We finish the item form by converting the item price to string. We haven't written the `ShoppingCart.total_item_price/1` function yet, but again we employed the idea of clear, descriptive public interfaces for our contexts. After rendering inputs for all the cart items, we show an "update cart" submit button, along with the total price of the entire cart. This is accomplished with another new `ShoppingCart.total_cart_price/1` function which we'll implement in a moment. Finally, we added a `back` component to go back to our products page. We're almost ready to try out our cart page, but first we need to implement our new currency calculation functions. Open up your shopping cart context at `lib/hello/shopping_cart.ex` and add these new functions: @@ -1034,7 +1027,7 @@ Head back over to your shopping cart context in `lib/hello/shopping_cart.ex` and end ``` -We started much like how our out-of-the-box code started – we take the cart struct and cast the user input to a cart changeset, except this time we use `Ecto.Changeset.cast_assoc/3` to cast the nested item data into `CartItem` changesets. Remember the [`<.inputs_for />`](`Phoenix.Component.inputs_for/1`) call in our cart form template? That hidden ID data is what allows Ecto's `cast_assoc` to map item data back to existing item associations in the cart. Next we use `Ecto.Multi.new/0`, which you may not have seen before. Ecto's `Multi` is a feature that allows lazily defining a chain of named operations to eventually execute inside a database transaction. Each operation in the multi chain receives the values from the previous steps and executes until a failed step is encountered. When an operation fails, the transaction is rolled back and an error is returned, otherwise the transaction is committed. +We started much like how our out-of-the-box code started – we take the cart struct and cast the user input to a cart changeset, except this time we use `Ecto.Changeset.cast_assoc/3` to cast the nested item data into `CartItem` changesets. Remember the [`<.inputs_for />`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#inputs_for/1) call in our cart form template? That hidden ID data is what allows Ecto's `cast_assoc` to map item data back to existing item associations in the cart. Next we use `Ecto.Multi.new/0`, which you may not have seen before. Ecto's `Multi` is a feature that allows lazily defining a chain of named operations to eventually execute inside a database transaction. Each operation in the multi chain receives the values from the previous steps and executes until a failed step is encountered. When an operation fails, the transaction is rolled back and an error is returned, otherwise the transaction is committed. For our multi operations, we start by issuing an update of our cart, which we named `:cart`. After the cart update is issued, we perform a multi `delete_all` operation, which takes the updated cart and applies our zero-quantity logic. We prune any items in the cart with zero quantity by returning an ecto query that finds all cart items for this cart with an empty quantity. Calling `Repo.transaction/1` with our multi will execute the operations in a new transaction and we return the success or failure result to the caller just like the original function. @@ -1067,7 +1060,7 @@ Remember to update your repository by running migrations: We generated an `Orders` context. We added a `user_uuid` field to associate our placeholder current user to an order, along with a `total_price` column. With our starting point in place, let's open up the newly created migration in `priv/repo/migrations/*_create_orders.exs` and make the following changes: -```elixir +```diff def change do create table(:orders) do add :user_uuid, :uuid @@ -1104,7 +1097,7 @@ Remember to update your repository by running migrations: We used the `phx.gen.context` command to generate the `LineItem` Ecto schema and inject supporting functions into our orders context. Like before, let's modify the migration in `priv/repo/migrations/*_create_order_line_items.exs` and make the following decimal field changes: -```elixir +```diff def change do create table(:order_line_items) do - add :price, :decimal @@ -1123,7 +1116,7 @@ We used the `phx.gen.context` command to generate the `LineItem` Ecto schema and With our migration in place, let's wire up our orders and line items associations in `lib/hello/orders/order.ex`: -```elixir +```diff schema "orders" do field :total_price, :decimal field :user_uuid, Ecto.UUID @@ -1137,7 +1130,7 @@ With our migration in place, let's wire up our orders and line items association We used `has_many :line_items` to associate orders and line items, just like we've seen before. Next, we used the `:through` feature of `has_many`, which allows us to instruct ecto how to associate resources across another relationship. In this case, we can associate products of an order by finding all products through associated line items. Next, let's wire up the association in the other direction in `lib/hello/orders/line_item.ex`: -```elixir +```diff schema "order_line_items" do field :price, :decimal field :quantity, :integer @@ -1153,7 +1146,7 @@ We used `has_many :line_items` to associate orders and line items, just like we' We used `belongs_to` to associate line items to orders and products. With our associations in place, we can start integrating the web interface into our order process. Open up your router `lib/hello_web/router.ex` and add the following line: -```elixir +```diff scope "/", HelloWeb do pipe_through :browser @@ -1299,20 +1292,20 @@ Next we can create the template at `lib/hello_web/controllers/order_html/show.ht <.header> Thank you for your order! <:subtitle> - User uuid: <%= @order.user_uuid %> + User uuid: {@order.user_uuid} <.table id="items" rows={@order.line_items}> - <:col :let={item} label="Title"><%= item.product.title %> - <:col :let={item} label="Quantity"><%= item.quantity %> + <:col :let={item} label="Title">{item.product.title} + <:col :let={item} label="Quantity">{item.quantity} <:col :let={item} label="Price"> - <%= HelloWeb.CartHTML.currency_to_str(item.price) %> + {HelloWeb.CartHTML.currency_to_str(item.price)} Total price: -<%= HelloWeb.CartHTML.currency_to_str(@order.total_price) %> +{HelloWeb.CartHTML.currency_to_str(@order.total_price)} <.back navigate={~p"/products"}>Back to products ``` @@ -1324,11 +1317,11 @@ Our last addition will be to add the "complete order" button to our cart page to ```diff <.header> My Cart -+ <:actions> -+ <.link href={~p"/orders"} method="post"> -+ <.button>Complete order -+ -+ ++ <:actions> ++ <.link href={~p"/orders"} method="post"> ++ <.button>Complete order ++ ++ ``` diff --git a/guides/introduction/up_and_running.md b/guides/introduction/up_and_running.md index df1871d49a..6b33dbb252 100644 --- a/guides/introduction/up_and_running.md +++ b/guides/introduction/up_and_running.md @@ -1,10 +1,34 @@ # Up and Running -Let's get a Phoenix application up and running as quickly as possible. +There are two mechanisms to start a new Phoenix application: the express option, supported on some OSes, and via `mix phx.new`. Let's check it out. -Before we begin, please take a minute to read the [Installation Guide](installation.html). By installing any necessary dependencies beforehand, we'll be able to get our application up and running smoothly. +## Phoenix Express -We can run `mix phx.new` from any directory in order to bootstrap our Phoenix application. Phoenix will accept either an absolute or relative path for the directory of our new project. Assuming that the name of our application is `hello`, let's run the following command: +A single command will get you up and running in seconds: + +For macOS/Ubuntu: + +```bash +$ curl https://new.phoenixframework.org/myapp | sh +``` + +For Windows PowerShell: + +```cmd +> curl.exe -fsSO https://new.phoenixframework.org/myapp.bat; .\myapp.bat +``` + +The above will install Erlang, Elixir, and Phoenix, and generate a fresh Phoenix application. It will also automatically pick one of PostgreSQL or MySQL as the database, and fallback to SQLite if none of them are available. Once the command above, it will open up a Phoenix application, with the steps necessary to complete your installation. + +> Your Phoenix application name is taken from the path. + +If your operating system is not supported, or the command above fails, don't fret! You can still start your Phoenix application using `mix phx.new`. + +## Via `mix phx.new` + +In order to create a new Phoenix application, you will need to install Erlang, Elixir, and Phoenix. See the [Installation Guide](installation.html) for more information. If you share your application with someone, they will also need to follow the Installation Guide steps to set it all up. + +Once you are ready, you can run `mix phx.new` from any directory in order to bootstrap our Phoenix application. Phoenix will accept either an absolute or relative path for the directory of our new project. Assuming that the name of our application is `hello`, let's run the following command: ```console $ mix phx.new hello diff --git a/guides/plug.md b/guides/plug.md index 0c1d6a5175..04e502a48d 100644 --- a/guides/plug.md +++ b/guides/plug.md @@ -120,7 +120,7 @@ In the [`init/1`] callback, we pass a default locale to use if none is present i To see the assign in action, go to the template in `lib/hello_web/controllers/page_html/home.html.heex` and add the following code after the closing of the `` tag: ```heex -

Locale: <%= @locale %>

+

Locale: {@locale}

``` Go to [http://localhost:4000/](http://localhost:4000/) and you should see the locale exhibited. Visit [http://localhost:4000/?locale=fr](http://localhost:4000/?locale=fr) and you should see the assign changed to `"fr"`. Someone can use this information alongside [Gettext](https://hexdocs.pm/gettext/Gettext.html) to provide a fully internationalized web application. diff --git a/guides/real_time/presence.md b/guides/real_time/presence.md index 106ea41775..32271b855a 100644 --- a/guides/real_time/presence.md +++ b/guides/real_time/presence.md @@ -217,7 +217,7 @@ defmodule HelloWeb.OnlineLive do def render(assigns) do ~H""" """ end diff --git a/guides/request_lifecycle.md b/guides/request_lifecycle.md index 177b9376ab..79ac1e9649 100644 --- a/guides/request_lifecycle.md +++ b/guides/request_lifecycle.md @@ -181,7 +181,7 @@ Now that we've got the route, controller, view, and template, we should be able There are a couple of interesting things to notice about what we just did. We didn't need to stop and restart the server while we made these changes. Yes, Phoenix has hot code reloading! Also, even though our `index.html.heex` file consists of only a single `section` tag, the page we get is a full HTML document. Our index template is actually rendered into layouts: first it renders `lib/hello_web/components/layouts/root.html.heex` which renders `lib/hello_web/components/layouts/app.html.heex` which finally includes our content. If you open those files, you'll see a line that looks like this at the bottom: ```heex -<%= @inner_content %> +{@inner_content} ``` This line injects our template into the layout before the HTML is sent off to the browser. We will talk more about layouts in the Controllers guide. @@ -273,15 +273,17 @@ It's good to remember that the keys of the `params` map will always be strings, For the last piece of this puzzle, we'll need a new template. Since it is for the `show` action of `HelloController`, it will go into the `lib/hello_web/controllers/hello_html` directory and be called `show.html.heex`. It will look surprisingly like our `index.html.heex` template, except that we will need to display the name of our messenger. -To do that, we'll use the special HEEx tags for executing Elixir expressions: `<%= %>`. Notice that the initial tag has an equals sign like this: `<%=` . That means that any Elixir code that goes between those tags will be executed, and the resulting value will replace the tag in the HTML output. If the equals sign were missing, the code would still be executed, but the value would not appear on the page. +To do that, we'll use the special HEEx tags for executing Elixir expressions: `{...}` and `<%= %>`. Notice that EEx tag has an equals sign like this: `<%=` . That means that any Elixir code that goes between those tags will be executed, and the resulting value will replace the tag in the HTML output. If the equals sign were missing, the code would still be executed, but the value would not appear on the page. -Remember our templates are written in HEEx (HTML+EEx). HEEx is a superset of EEx which is why it shares the `<%= %>` syntax. +Remember our templates are written in HEEx (HTML+EEx). HEEx is a superset of EEx, and thereby supports the EEx `<%= %>` interpolation syntax for interpolating arbitrary blocks of code. In general, the HEEx `{...}` interpolation syntax is preferred anytime there is HTML-aware intepolation to be done – such as within attributes or inline values with a body. -And this is what the template should look like: +The only times `EEx` `<%= %>` interpolation is necessary is for interpolationg arbitrary blocks of markup, such as branching logic that inects separate markup trees, or for interpolating values within ` - <%%= @inner_content %> + {@inner_content} diff --git a/installer/templates/phx_web/controllers/page_html/home.html.heex b/installer/templates/phx_web/controllers/page_html/home.html.heex index 071a7104d3..cd96076f08 100644 --- a/installer/templates/phx_web/controllers/page_html/home.html.heex +++ b/installer/templates/phx_web/controllers/page_html/home.html.heex @@ -50,7 +50,7 @@

Phoenix Framework - v<%%= Application.spec(:phoenix, :vsn) %> + v{Application.spec(:phoenix, :vsn)}

diff --git a/integration_test/mix.exs b/integration_test/mix.exs index 63cedf5317..a63f329ba0 100644 --- a/integration_test/mix.exs +++ b/integration_test/mix.exs @@ -41,8 +41,7 @@ defmodule Phoenix.Integration.MixProject do {:tds, ">= 0.0.0"}, {:ecto_sqlite3, ">= 0.0.0"}, {:phoenix_html, "~> 4.1"}, - # TODO bump on release to {:phoenix_live_view, "~> 1.0.0"}, - {:phoenix_live_view, "~> 1.0.0-rc.0", override: true}, + {:phoenix_live_view, "~> 1.0.0"}, {:dns_cluster, "~> 0.1.1"}, {:floki, ">= 0.30.0"}, {:phoenix_live_reload, "~> 1.2"}, diff --git a/integration_test/mix.lock b/integration_test/mix.lock index 6ce1c9a9ce..199ec925d0 100644 --- a/integration_test/mix.lock +++ b/integration_test/mix.lock @@ -1,51 +1,51 @@ %{ "argon2_elixir": {:hex, :argon2_elixir, "4.1.0", "2f242afe47c373663cb404eb75e792f749507075ed737b49685a9f2edcb401df", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ecb6f2ca2cca34b28e546224661bf2a85714516d2713c7313c5ffe8bdade7cf"}, - "bandit": {:hex, :bandit, "1.5.4", "8e56e7cfc06f3c57995be0d9bf4e45b972d8732f5c7e96ef8ec0735f52079527", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "04c2b38874769af67fe7f10034f606ad6dda1d8f80c4d7a0c616b347584d5aff"}, - "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"}, - "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"}, + "bandit": {:hex, :bandit, "1.6.1", "9e01b93d72ddc21d8c576a704949e86ee6cde7d11270a1d3073787876527a48f", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "5a904bf010ea24b67979835e0507688e31ac873d4ffc8ed0e5413e8d77455031"}, + "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.2.0", "feab711974beba4cb348147170346fe097eea2e840db4e012a145e180ed4ab75", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "563e92a6c77d667b19c5f4ba17ab6d440a085696bdf4c68b9b0f5b30bc5422b8"}, + "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"}, "comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"}, - "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"}, + "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.2.0", "df3d06bb9517e302b1bd265c1e7f16cda51547ad9d99892049340841f3e15836", [:mix], [], "hexpm", "af8daf87384b51b7e611fb1a1f2c4d4876b65ef968fa8bd3adf44cff401c7f21"}, "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"}, - "ecto": {:hex, :ecto, "3.11.2", "e1d26be989db350a633667c5cda9c3d115ae779b66da567c68c80cfb26a8c9ee", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3c38bca2c6f8d8023f2145326cc8a80100c3ffe4dcbd9842ff867f7fc6156c65"}, - "ecto_sql": {:hex, :ecto_sql, "3.11.3", "4eb7348ff8101fbc4e6bbc5a4404a24fecbe73a3372d16569526b0cf34ebc195", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e5f36e3d736b99c7fee3e631333b8394ade4bafe9d96d35669fca2d81c2be928"}, - "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.16.0", "1cdc8ea6319e7cb1bc273a36db0ecde69ad56b4dea3037689ad8c0afc6a91e16", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "73c9dd56830d67c951bc254c082cb0a7f9fa139d44866bc3186c8859d1b4d787"}, - "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"}, - "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"}, + "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, + "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, + "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.17.5", "fbee5c17ff6afd8e9ded519b0abb363926c65d30b27577232bb066b2a79957b8", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "3b54734d998cbd032ac59403c36acf4e019670e8b6ceef9c6c33d8986c4e9704"}, + "elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"}, + "esbuild": {:hex, :esbuild, "0.8.2", "5f379dfa383ef482b738e7771daf238b2d1cfb0222bef9d3b20d4c8f06c7a7ac", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "558a8a08ed78eb820efbfda1de196569d8bfa9b51e8371a1934fbb31345feda7"}, "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"}, - "exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"}, - "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, - "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, - "floki": {:hex, :floki, "0.36.3", "1102f93b16a55bc5383b85ae3ec470f82dee056eaeff9195e8afdf0ef2a43c30", [:mix], [], "hexpm", "fe0158bff509e407735f6d40b3ee0d7deb47f3f3ee7c6c182ad28599f9f6b27a"}, - "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"}, + "exqlite": {:hex, :exqlite, "0.27.1", "73fc0b3dc3b058a77a2b3771f82a6af2ddcf370b069906968a34083d2ffd2884", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "79ef5756451cfb022e8013e1ed00d0f8f7d1333c19502c394dc16b15cfb4e9b4"}, + "file_system": {:hex, :file_system, "1.0.1", "79e8ceaddb0416f8b8cd02a0127bdbababe7bf4a23d2a395b983c1f8b3f73edd", [:mix], [], "hexpm", "4414d1f38863ddf9120720cd976fce5bdde8e91d8283353f0e31850fa89feb9e"}, + "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, + "floki": {:hex, :floki, "0.37.0", "b83e0280bbc6372f2a403b2848013650b16640cd2470aea6701f0632223d719e", [:mix], [], "hexpm", "516a0c15a69f78c47dc8e0b9b3724b29608aa6619379f91b1ffa47109b5d0dd3"}, + "gettext": {:hex, :gettext, "0.26.2", "5978aa7b21fada6deabf1f6341ddba50bc69c999e812211903b169799208f2a8", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "aa978504bcf76511efdc22d580ba08e2279caab1066b76bb9aa81c4a1e0a32a5"}, "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]}, - "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, + "hpax": {:hex, :hpax, "1.0.1", "c857057f89e8bd71d97d9042e009df2a42705d6d690d54eca84c8b29af0787b0", [:mix], [], "hexpm", "4e2d5a4f76ae1e3048f35ae7adb1641c36265510a2d4638157fbcb53dda38445"}, "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, - "myxql": {:hex, :myxql, "0.7.0", "3382f139b0b0da977a8fc33c8cded125e20df2e400f8d7b7e674fa62a7e077dd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "40e4b7ad4973c8b895e86a3de04ff7a79c2cf72b9f2bddef7717afb4ab36d8c0"}, + "myxql": {:hex, :myxql, "0.7.1", "7c7b75aa82227cd2bc9b7fbd4de774fb19a1cdb309c219f411f82ca8860f8e01", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:geo, "~> 3.4", [hex: :geo, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a491cdff53353a09b5850ac2d472816ebe19f76c30b0d36a43317a67c9004936"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, - "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "2.2.0", "2ec4f7daae2bf74cb9e52df3554bbdcec8a38104a7f0ccaa4d45d5919e4c3f19", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "6c4af97f5cae925c56caded648520510ea583eebf1587e185b9f445762197aff"}, - "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.1", "96798325fab2fed5a824ca204e877b81f9afd2e480f581e81f7b4b64a5a477f2", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.17", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "0ae544ff99f3c482b0807c5cec2c8289e810ecacabc04959d82c3337f4703391"}, + "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "2.3.0", "7b3967797963e15312cde8beeac29136238bb020b1da60c4a0ede610dd9115c1", [:mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}], "hexpm", "61a7bba2764ea03e68a1b8822fdf3e486149bf2caf8f7adf752e3e4aea4467bc"}, + "phoenix_ecto": {:hex, :phoenix_ecto, "4.6.3", "f686701b0499a07f2e3b122d84d52ff8a31f5def386e03706c916f6feddf69ef", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.1", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "909502956916a657a197f94cc1206d9a65247538de8a5e186f7537c895d95764"}, "phoenix_html": {:hex, :phoenix_html, "4.1.1", "4c064fd3873d12ebb1388425a8f2a19348cef56e7289e1998e2d2fa758aa982e", [:mix], [], "hexpm", "f2f2df5a72bc9a2f510b21497fd7d2b86d932ec0598f0210fed4114adc546c6f"}, - "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.3", "7ff51c9b6609470f681fbea20578dede0e548302b0c8bdf338b5a753a4f045bf", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "f9470a0a8bae4f56430a23d42f977b5a6205fdba6559d76f932b876bfaec652d"}, + "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.5", "d5f44d7dbd7cfacaa617b70c5a14b2b598d6f93b9caa8e350c51d56cd4350a9b", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "1d73920515554d7d6c548aee0bf10a4780568b029d042eccb336db29ea0dad70"}, "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.5.3", "f2161c207fda0e4fb55165f650f7f8db23f02b29e3bff00ff7ef161d6ac1f09d", [:mix], [{:file_system, "~> 0.3 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b4ec9cd73cb01ff1bd1cac92e045d13e7030330b74164297d1aee3907b54803c"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0-rc.7", "d2abca526422adea88896769529addb6443390b1d4f1ff9cbe694312d8875fb2", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b82a4575f6f3eb5b97922ec6874b0c52b3ca0cc5dcb4b14ddc478cbfa135dd01"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "1.0.0", "3a10dfce8f87b2ad4dc65de0732fc2a11e670b2779a19e8d3281f4619a85bce4", [:mix], [{:floki, "~> 0.36", [hex: :floki, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "254caef0028765965ca6bd104cc7d68dcc7d57cc42912bef92f6b03047251d99"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"}, "phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"}, "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"}, - "postgrex": {:hex, :postgrex, "0.18.0", "f34664101eaca11ff24481ed4c378492fed2ff416cd9b06c399e90f321867d7e", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a042989ba1bc1cca7383ebb9e461398e3f89f868c92ce6671feb7ef132a252d1"}, - "req": {:hex, :req, "0.5.4", "e375e4812adf83ffcf787871d7a124d873e983e3b77466e6608b973582f7f837", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "a17998ffe2ef54f79bfdd782ef9f4cbf987d93851e89444cbc466a6a25eee494"}, - "swoosh": {:hex, :swoosh, "1.16.9", "20c6a32ea49136a4c19f538e27739bb5070558c0fa76b8a95f4d5d5ca7d319a1", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.0", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "878b1a7a6c10ebbf725a3349363f48f79c5e3d792eb621643b0d276a38acc0a6"}, - "tailwind": {:hex, :tailwind, "0.2.3", "277f08145d407de49650d0a4685dc062174bdd1ae7731c5f1da86163a24dfcdb", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "8e45e7a34a676a7747d04f7913a96c770c85e6be810a1d7f91e713d3a3655b5d"}, + "postgrex": {:hex, :postgrex, "0.19.3", "a0bda6e3bc75ec07fca5b0a89bffd242ca209a4822a9533e7d3e84ee80707e19", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "d31c28053655b78f47f948c85bb1cf86a9c1f8ead346ba1aa0d0df017fa05b61"}, + "req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"}, + "swoosh": {:hex, :swoosh, "1.17.3", "5cda7bff6bc1121cc5b58db8ed90ef33261b373425ae3e32dd599688037a0482", [:mix], [{:bandit, ">= 1.0.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:cowboy, "~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:ex_aws, "~> 2.1", [hex: :ex_aws, repo: "hexpm", optional: true]}, {:finch, "~> 0.6", [hex: :finch, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13 or ~> 1.0", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mua, "~> 0.2.3", [hex: :mua, repo: "hexpm", optional: true]}, {:multipart, "~> 0.4", [hex: :multipart, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:req, "~> 0.5 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "14ad57cfbb70af57323e17f569f5840a33c01f8ebc531dd3846beef3c9c95e55"}, + "tailwind": {:hex, :tailwind, "0.2.4", "5706ec47182d4e7045901302bf3a333e80f3d1af65c442ba9a9eed152fb26c2e", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "c6e4a82b8727bab593700c998a4d98cf3d8025678bfde059aed71d0000c3e463"}, "tds": {:hex, :tds, "2.3.5", "fedfb96d53206f01eac62ead859e47e1541a62e1553e9eb7a8801c7dca59eae8", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "52e350f5dd5584bbcff9859e331be144d290b41bd4c749b936014a17660662f2"}, "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"}, "telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"}, - "thousand_island": {:hex, :thousand_island, "1.3.5", "6022b6338f1635b3d32406ff98d68b843ba73b3aa95cfc27154223244f3a6ca5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2be6954916fdfe4756af3239fb6b6d75d0b8063b5df03ba76fd8a4c87849e180"}, + "thousand_island": {:hex, :thousand_island, "1.3.7", "1da7598c0f4f5f50562c097a3f8af308ded48cd35139f0e6f17d9443e4d0c9c5", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "0139335079953de41d381a6134d8b618d53d084f558c734f2662d1a72818dd12"}, "websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"}, - "websock_adapter": {:hex, :websock_adapter, "0.5.6", "0437fe56e093fd4ac422de33bf8fc89f7bc1416a3f2d732d8b2c8fd54792fe60", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "e04378d26b0af627817ae84c92083b7e97aca3121196679b73c73b99d0d133ea"}, + "websock_adapter": {:hex, :websock_adapter, "0.5.8", "3b97dc94e407e2d1fc666b2fb9acf6be81a1798a2602294aac000260a7c4a47d", [:mix], [{:bandit, ">= 0.6.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "315b9a1865552212b5f35140ad194e67ce31af45bcee443d4ecb96b5fd3f3782"}, } diff --git a/lib/mix/phoenix.ex b/lib/mix/phoenix.ex index 7c6b8e6d53..78181df0b1 100644 --- a/lib/mix/phoenix.ex +++ b/lib/mix/phoenix.ex @@ -43,8 +43,12 @@ defmodule Mix.Phoenix do end) || raise "could not find #{source_file_path} in any of the sources" case format do - :text -> Mix.Generator.create_file(target, File.read!(source)) - :eex -> Mix.Generator.create_file(target, EEx.eval_file(source, binding)) + :text -> + Mix.Generator.create_file(target, File.read!(source)) + + :eex -> + Mix.Generator.create_file(target, EEx.eval_file(source, binding)) + :new_eex -> if File.exists?(target) do :ok @@ -57,6 +61,7 @@ defmodule Mix.Phoenix do defp to_app_source(path, source_dir) when is_binary(path), do: Path.join(path, source_dir) + defp to_app_source(app, source_dir) when is_atom(app), do: Application.app_dir(app, source_dir) @@ -97,23 +102,25 @@ defmodule Mix.Phoenix do """ def inflect(singular) do - base = Mix.Phoenix.base() + base = Mix.Phoenix.base() web_module = base |> web_module() |> inspect() - scoped = Phoenix.Naming.camelize(singular) - path = Phoenix.Naming.underscore(scoped) - singular = String.split(path, "/") |> List.last - module = Module.concat(base, scoped) |> inspect - alias = String.split(module, ".") |> List.last - human = Phoenix.Naming.humanize(singular) - - [alias: alias, - human: human, - base: base, - web_module: web_module, - module: module, - scoped: scoped, - singular: singular, - path: path] + scoped = Phoenix.Naming.camelize(singular) + path = Phoenix.Naming.underscore(scoped) + singular = String.split(path, "/") |> List.last() + module = Module.concat(base, scoped) |> inspect + alias = String.split(module, ".") |> List.last() + human = Phoenix.Naming.humanize(singular) + + [ + alias: alias, + human: human, + base: base, + web_module: web_module, + module: module, + scoped: scoped, + singular: singular, + path: path + ] end @doc """ @@ -121,8 +128,9 @@ defmodule Mix.Phoenix do """ def check_module_name_availability!(name) do name = Module.concat(Elixir, name) + if Code.ensure_loaded?(name) do - Mix.raise "Module name #{inspect name} is already taken, please choose another name" + Mix.raise("Module name #{inspect(name)} is already taken, please choose another name") end end @@ -151,7 +159,7 @@ defmodule Mix.Phoenix do defp app_base(app) do case Application.get_env(app, :namespace, app) do ^app -> app |> to_string() |> Phoenix.Naming.camelize() - mod -> mod |> inspect() + mod -> mod |> inspect() end end @@ -190,7 +198,7 @@ defmodule Mix.Phoenix do Checks if the given `app_path` is inside an umbrella. """ def in_umbrella?(app_path) do - umbrella = Path.expand(Path.join [app_path, "..", ".."]) + umbrella = Path.expand(Path.join([app_path, "..", ".."])) mix_path = Path.join(umbrella, "mix.exs") apps_path = Path.join(umbrella, "apps") File.exists?(mix_path) && File.exists?(apps_path) @@ -223,6 +231,7 @@ defmodule Mix.Phoenix do {^ctx_app, path} -> Path.relative_to_cwd(path) _ -> mix_app_path(ctx_app, this_app) end + Path.join(app_path, rel_path) end end @@ -270,8 +279,9 @@ defmodule Mix.Phoenix do case Application.get_env(this_otp_app, :generators)[:context_app] do nil -> :error + false -> - Mix.raise """ + Mix.raise(""" no context_app configured for current application #{this_otp_app}. Add the context_app generators config in config.exs, or pass the @@ -288,9 +298,11 @@ defmodule Mix.Phoenix do Note: cli option only works when `context_app` is not set to `false` in the config. - """ + """) + {app, _path} -> {:ok, app} + app -> {:ok, app} end @@ -300,11 +312,12 @@ defmodule Mix.Phoenix do case Mix.Project.deps_paths() do %{^app => path} -> Path.relative_to_cwd(path) + deps -> - Mix.raise """ - no directory for context_app #{inspect app} found in #{this_otp_app}'s deps. + Mix.raise(""" + no directory for context_app #{inspect(app)} found in #{this_otp_app}'s deps. - Ensure you have listed #{inspect app} as an in_umbrella dependency in mix.exs: + Ensure you have listed #{inspect(app)} as an in_umbrella dependency in mix.exs: def deps do [ @@ -315,9 +328,9 @@ defmodule Mix.Phoenix do Existing deps: - #{inspect Map.keys(deps)} + #{inspect(Map.keys(deps))} - """ + """) end end @@ -332,15 +345,18 @@ defmodule Mix.Phoenix do end) case Enum.filter(file_paths, &File.exists?(&1)) do - [] -> :ok + [] -> + :ok + conflicts -> - Mix.shell().info""" + Mix.shell().info(""" The following files conflict with new files to be generated: #{Enum.map_join(conflicts, "\n", &" * #{&1}")} See the --web option to namespace similarly named resources - """ + """) + unless Mix.shell().yes?("Proceed with interactive overwrite?") do System.halt() end @@ -359,13 +375,25 @@ defmodule Mix.Phoenix do end def to_text(data) do - inspect data, limit: :infinity, printable_limit: :infinity + inspect(data, limit: :infinity, printable_limit: :infinity) end def prepend_newline(string) do "\n" <> string end + @doc """ + Ensures user's LiveView is compatible with the current generators. + """ + def ensure_live_view_compat!(generator_mod) do + vsn = Application.spec(:phoenix_live_view)[:vsn] + + # if lv is not installed, such as in phoenix's own test env, do not raise + if vsn && Version.compare("#{vsn}", "1.0.0-rc.7") != :gt do + raise "#{inspect(generator_mod)} requires :phoenix_live_view >= 1.0.0, got: #{vsn}" + end + end + # In the context of a HEEx attribute value, transforms a given message into a # dynamic `gettext` call or a fixed-value string attribute, depending on the # `gettext?` parameter. diff --git a/lib/mix/phoenix/context.ex b/lib/mix/phoenix/context.ex index 27fbc65622..b4134de33c 100644 --- a/lib/mix/phoenix/context.ex +++ b/lib/mix/phoenix/context.ex @@ -27,15 +27,15 @@ defmodule Mix.Phoenix.Context do end def new(context_name, %Schema{} = schema, opts) do - ctx_app = opts[:context_app] || Mix.Phoenix.context_app() - base = Module.concat([Mix.Phoenix.context_base(ctx_app)]) - module = Module.concat(base, context_name) - alias = Module.concat([module |> Module.split() |> List.last()]) - basedir = Phoenix.Naming.underscore(context_name) - basename = Path.basename(basedir) - dir = Mix.Phoenix.context_lib_path(ctx_app, basedir) - file = dir <> ".ex" - test_dir = Mix.Phoenix.context_test_path(ctx_app, basedir) + ctx_app = opts[:context_app] || Mix.Phoenix.context_app() + base = Module.concat([Mix.Phoenix.context_base(ctx_app)]) + module = Module.concat(base, context_name) + alias = Module.concat([module |> Module.split() |> List.last()]) + basedir = Phoenix.Naming.underscore(context_name) + basename = Path.basename(basedir) + dir = Mix.Phoenix.context_lib_path(ctx_app, basedir) + file = dir <> ".ex" + test_dir = Mix.Phoenix.context_test_path(ctx_app, basedir) test_file = test_dir <> "_test.exs" test_fixtures_dir = Mix.Phoenix.context_app_path(ctx_app, "test/support/fixtures") test_fixtures_file = Path.join([test_fixtures_dir, basedir <> "_fixtures.ex"]) diff --git a/lib/mix/tasks/phx.gen.auth.ex b/lib/mix/tasks/phx.gen.auth.ex index 5f636a5cce..786bac8fb5 100644 --- a/lib/mix/tasks/phx.gen.auth.ex +++ b/lib/mix/tasks/phx.gen.auth.ex @@ -126,6 +126,8 @@ defmodule Mix.Tasks.Phx.Gen.Auth do Mix.raise("mix phx.gen.auth can only be run inside an application directory") end + Mix.Phoenix.ensure_live_view_compat!(__MODULE__) + {opts, parsed} = OptionParser.parse!(args, strict: @switches) validate_args!(parsed) hashing_library = build_hashing_library!(opts) diff --git a/lib/mix/tasks/phx.gen.auth/injector.ex b/lib/mix/tasks/phx.gen.auth/injector.ex index 48ae16782c..cc891bc70b 100644 --- a/lib/mix/tasks/phx.gen.auth/injector.ex +++ b/lib/mix/tasks/phx.gen.auth/injector.ex @@ -170,7 +170,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do