Setup AGENTS.md and CLAUDE.md
This commit is contained in:
@@ -0,0 +1,555 @@
|
|||||||
|
<!-- usage-rules-start -->
|
||||||
|
<!-- usage-rules-header -->
|
||||||
|
# Usage Rules
|
||||||
|
|
||||||
|
**IMPORTANT**: Consult these usage rules early and often when working with the packages listed below.
|
||||||
|
Before attempting to use any of these packages or to discover if you should use them, review their
|
||||||
|
usage rules to understand the correct patterns, conventions, and best practices.
|
||||||
|
<!-- usage-rules-header-end -->
|
||||||
|
|
||||||
|
<!-- phoenix:ecto-start -->
|
||||||
|
## phoenix:ecto usage
|
||||||
|
## Ecto Guidelines
|
||||||
|
|
||||||
|
- **Always** preload Ecto associations in queries when they'll be accessed in templates, ie a message that needs to reference the `message.user.email`
|
||||||
|
- Remember `import Ecto.Query` and other supporting modules when you write `seeds.exs`
|
||||||
|
- `Ecto.Schema` fields always use the `:string` type, even for `:text`, columns, ie: `field :name, :string`
|
||||||
|
- `Ecto.Changeset.validate_number/2` **DOES NOT SUPPORT the `:allow_nil` option**. By default, Ecto validations only run if a change for the given field exists and the change value is not nil, so such as option is never needed
|
||||||
|
- You **must** use `Ecto.Changeset.get_field(changeset, :field)` to access changeset fields
|
||||||
|
- Fields which are set programatically, such as `user_id`, must not be listed in `cast` calls or similar for security purposes. Instead they must be explicitly set when creating the struct
|
||||||
|
- **Always** invoke `mix ecto.gen.migration migration_name_using_underscores` when generating migration files, so the correct timestamp and conventions are applied
|
||||||
|
|
||||||
|
<!-- phoenix:ecto-end -->
|
||||||
|
<!-- phoenix:elixir-start -->
|
||||||
|
## phoenix:elixir usage
|
||||||
|
## Elixir guidelines
|
||||||
|
|
||||||
|
- Elixir lists **do not support index based access via the access syntax**
|
||||||
|
|
||||||
|
**Never do this (invalid)**:
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
mylist = ["blue", "green"]
|
||||||
|
mylist[i]
|
||||||
|
|
||||||
|
Instead, **always** use `Enum.at`, pattern matching, or `List` for index based list access, ie:
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
mylist = ["blue", "green"]
|
||||||
|
Enum.at(mylist, i)
|
||||||
|
|
||||||
|
- Elixir variables are immutable, but can be rebound, so for block expressions like `if`, `case`, `cond`, etc
|
||||||
|
you *must* bind the result of the expression to a variable if you want to use it and you CANNOT rebind the result inside the expression, ie:
|
||||||
|
|
||||||
|
# INVALID: we are rebinding inside the `if` and the result never gets assigned
|
||||||
|
if connected?(socket) do
|
||||||
|
socket = assign(socket, :val, val)
|
||||||
|
end
|
||||||
|
|
||||||
|
# VALID: we rebind the result of the `if` to a new variable
|
||||||
|
socket =
|
||||||
|
if connected?(socket) do
|
||||||
|
assign(socket, :val, val)
|
||||||
|
end
|
||||||
|
|
||||||
|
- **Never** nest multiple modules in the same file as it can cause cyclic dependencies and compilation errors
|
||||||
|
- **Never** use map access syntax (`changeset[:field]`) on structs as they do not implement the Access behaviour by default. For regular structs, you **must** access the fields directly, such as `my_struct.field` or use higher level APIs that are available on the struct if they exist, `Ecto.Changeset.get_field/2` for changesets
|
||||||
|
- Elixir's standard library has everything necessary for date and time manipulation. Familiarize yourself with the common `Time`, `Date`, `DateTime`, and `Calendar` interfaces by accessing their documentation as necessary. **Never** install additional dependencies unless asked or for date/time parsing (which you can use the `date_time_parser` package)
|
||||||
|
- Don't use `String.to_atom/1` on user input (memory leak risk)
|
||||||
|
- Predicate function names should not start with `is_` and should end in a question mark. Names like `is_thing` should be reserved for guards
|
||||||
|
- Elixir's builtin OTP primitives like `DynamicSupervisor` and `Registry`, require names in the child spec, such as `{DynamicSupervisor, name: MyApp.MyDynamicSup}`, then you can use `DynamicSupervisor.start_child(MyApp.MyDynamicSup, child_spec)`
|
||||||
|
- Use `Task.async_stream(collection, callback, options)` for concurrent enumeration with back-pressure. The majority of times you will want to pass `timeout: :infinity` as option
|
||||||
|
|
||||||
|
## Mix guidelines
|
||||||
|
|
||||||
|
- Read the docs and options before using tasks (by using `mix help task_name`)
|
||||||
|
- To debug test failures, run tests in a specific file with `mix test test/my_test.exs` or run all previously failed tests with `mix test --failed`
|
||||||
|
- `mix deps.clean --all` is **almost never needed**. **Avoid** using it unless you have good reason
|
||||||
|
|
||||||
|
## Test guidelines
|
||||||
|
|
||||||
|
- **Always use `start_supervised!/1`** to start processes in tests as it guarantees cleanup between tests
|
||||||
|
- **Avoid** `Process.sleep/1` and `Process.alive?/1` in tests
|
||||||
|
- Instead of sleeping to wait for a process to finish, **always** use `Process.monitor/1` and assert on the DOWN message:
|
||||||
|
|
||||||
|
ref = Process.monitor(pid)
|
||||||
|
assert_receive {:DOWN, ^ref, :process, ^pid, :normal}
|
||||||
|
|
||||||
|
- Instead of sleeping to synchronize before the next call, **always** use `_ = :sys.get_state/1` to ensure the process has handled prior messages
|
||||||
|
|
||||||
|
|
||||||
|
<!-- phoenix:elixir-end -->
|
||||||
|
<!-- phoenix:html-start -->
|
||||||
|
## phoenix:html usage
|
||||||
|
## Phoenix HTML guidelines
|
||||||
|
|
||||||
|
- Phoenix templates **always** use `~H` or .html.heex files (known as HEEx), **never** use `~E`
|
||||||
|
- **Always** use the imported `Phoenix.Component.form/1` and `Phoenix.Component.inputs_for/1` function to build forms. **Never** use `Phoenix.HTML.form_for` or `Phoenix.HTML.inputs_for` as they are outdated
|
||||||
|
- When building forms **always** use the already imported `Phoenix.Component.to_form/2` (`assign(socket, form: to_form(...))` and `<.form for={@form} id="msg-form">`), then access those forms in the template via `@form[:field]`
|
||||||
|
- **Always** add unique DOM IDs to key elements (like forms, buttons, etc) when writing templates, these IDs can later be used in tests (`<.form for={@form} id="product-form">`)
|
||||||
|
- For "app wide" template imports, you can import/alias into the `my_app_web.ex`'s `html_helpers` block, so they will be available to all LiveViews, LiveComponent's, and all modules that do `use MyAppWeb, :html` (replace "my_app" by the actual app name)
|
||||||
|
|
||||||
|
- Elixir supports `if/else` but **does NOT support `if/else if` or `if/elsif`**. **Never use `else if` or `elseif` in Elixir**, **always** use `cond` or `case` for multiple conditionals.
|
||||||
|
|
||||||
|
**Never do this (invalid)**:
|
||||||
|
|
||||||
|
<%= if condition do %>
|
||||||
|
...
|
||||||
|
<% else if other_condition %>
|
||||||
|
...
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
Instead **always** do this:
|
||||||
|
|
||||||
|
<%= cond do %>
|
||||||
|
<% condition -> %>
|
||||||
|
...
|
||||||
|
<% condition2 -> %>
|
||||||
|
...
|
||||||
|
<% true -> %>
|
||||||
|
...
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
- HEEx require special tag annotation if you want to insert literal curly's like `{` or `}`. If you want to show a textual code snippet on the page in a `<pre>` or `<code>` block you *must* annotate the parent tag with `phx-no-curly-interpolation`:
|
||||||
|
|
||||||
|
<code phx-no-curly-interpolation>
|
||||||
|
let obj = {key: "val"}
|
||||||
|
</code>
|
||||||
|
|
||||||
|
Within `phx-no-curly-interpolation` annotated tags, you can use `{` and `}` without escaping them, and dynamic Elixir expressions can still be used with `<%= ... %>` syntax
|
||||||
|
|
||||||
|
- HEEx class attrs support lists, but you must **always** use list `[...]` syntax. You can use the class list syntax to conditionally add classes, **always do this for multiple class values**:
|
||||||
|
|
||||||
|
<a class={[
|
||||||
|
"px-2 text-white",
|
||||||
|
@some_flag && "py-5",
|
||||||
|
if(@other_condition, do: "border-red-500", else: "border-blue-100"),
|
||||||
|
...
|
||||||
|
]}>Text</a>
|
||||||
|
|
||||||
|
and **always** wrap `if`'s inside `{...}` expressions with parens, like done above (`if(@other_condition, do: "...", else: "...")`)
|
||||||
|
|
||||||
|
and **never** do this, since it's invalid (note the missing `[` and `]`):
|
||||||
|
|
||||||
|
<a class={
|
||||||
|
"px-2 text-white",
|
||||||
|
@some_flag && "py-5"
|
||||||
|
}> ...
|
||||||
|
=> Raises compile syntax error on invalid HEEx attr syntax
|
||||||
|
|
||||||
|
- **Never** use `<% Enum.each %>` or non-for comprehensions for generating template content, instead **always** use `<%= for item <- @collection do %>`
|
||||||
|
- HEEx HTML comments use `<%!-- comment --%>`. **Always** use the HEEx HTML comment syntax for template comments (`<%!-- comment --%>`)
|
||||||
|
- HEEx allows interpolation via `{...}` and `<%= ... %>`, but the `<%= %>` **only** works within tag bodies. **Always** use the `{...}` syntax for interpolation within tag attributes, and for interpolation of values within tag bodies. **Always** interpolate block constructs (if, cond, case, for) within tag bodies using `<%= ... %>`.
|
||||||
|
|
||||||
|
**Always** do this:
|
||||||
|
|
||||||
|
<div id={@id}>
|
||||||
|
{@my_assign}
|
||||||
|
<%= if @some_block_condition do %>
|
||||||
|
{@another_assign}
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
and **Never** do this – the program will terminate with a syntax error:
|
||||||
|
|
||||||
|
<%!-- THIS IS INVALID NEVER EVER DO THIS --%>
|
||||||
|
<div id="<%= @invalid_interpolation %>">
|
||||||
|
{if @invalid_block_construct do}
|
||||||
|
{end}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- phoenix:html-end -->
|
||||||
|
<!-- phoenix:liveview-start -->
|
||||||
|
## phoenix:liveview usage
|
||||||
|
## Phoenix LiveView guidelines
|
||||||
|
|
||||||
|
- **Never** use the deprecated `live_redirect` and `live_patch` functions, instead **always** use the `<.link navigate={href}>` and `<.link patch={href}>` in templates, and `push_navigate` and `push_patch` functions LiveViews
|
||||||
|
- **Avoid LiveComponent's** unless you have a strong, specific need for them
|
||||||
|
- LiveViews should be named like `AppWeb.WeatherLive`, with a `Live` suffix. When you go to add LiveView routes to the router, the default `:browser` scope is **already aliased** with the `AppWeb` module, so you can just do `live "/weather", WeatherLive`
|
||||||
|
|
||||||
|
### LiveView streams
|
||||||
|
|
||||||
|
- **Always** use LiveView streams for collections for assigning regular lists to avoid memory ballooning and runtime termination with the following operations:
|
||||||
|
- basic append of N items - `stream(socket, :messages, [new_msg])`
|
||||||
|
- resetting stream with new items - `stream(socket, :messages, [new_msg], reset: true)` (e.g. for filtering items)
|
||||||
|
- prepend to stream - `stream(socket, :messages, [new_msg], at: -1)`
|
||||||
|
- deleting items - `stream_delete(socket, :messages, msg)`
|
||||||
|
|
||||||
|
- When using the `stream/3` interfaces in the LiveView, the LiveView template must 1) always set `phx-update="stream"` on the parent element, with a DOM id on the parent element like `id="messages"` and 2) consume the `@streams.stream_name` collection and use the id as the DOM id for each child. For a call like `stream(socket, :messages, [new_msg])` in the LiveView, the template would be:
|
||||||
|
|
||||||
|
<div id="messages" phx-update="stream">
|
||||||
|
<div :for={{id, msg} <- @streams.messages} id={id}>
|
||||||
|
{msg.text}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
- LiveView streams are *not* enumerable, so you cannot use `Enum.filter/2` or `Enum.reject/2` on them. Instead, if you want to filter, prune, or refresh a list of items on the UI, you **must refetch the data and re-stream the entire stream collection, passing reset: true**:
|
||||||
|
|
||||||
|
def handle_event("filter", %{"filter" => filter}, socket) do
|
||||||
|
# re-fetch the messages based on the filter
|
||||||
|
messages = list_messages(filter)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> assign(:messages_empty?, messages == [])
|
||||||
|
# reset the stream with the new messages
|
||||||
|
|> stream(:messages, messages, reset: true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
- LiveView streams *do not support counting or empty states*. If you need to display a count, you must track it using a separate assign. For empty states, you can use Tailwind classes:
|
||||||
|
|
||||||
|
<div id="tasks" phx-update="stream">
|
||||||
|
<div class="hidden only:block">No tasks yet</div>
|
||||||
|
<div :for={{id, task} <- @stream.tasks} id={id}>
|
||||||
|
{task.name}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
The above only works if the empty state is the only HTML block alongside the stream for-comprehension.
|
||||||
|
|
||||||
|
- When updating an assign that should change content inside any streamed item(s), you MUST re-stream the items
|
||||||
|
along with the updated assign:
|
||||||
|
|
||||||
|
def handle_event("edit_message", %{"message_id" => message_id}, socket) do
|
||||||
|
message = Chat.get_message!(message_id)
|
||||||
|
edit_form = to_form(Chat.change_message(message, %{content: message.content}))
|
||||||
|
|
||||||
|
# re-insert message so @editing_message_id toggle logic takes effect for that stream item
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> stream_insert(:messages, message)
|
||||||
|
|> assign(:editing_message_id, String.to_integer(message_id))
|
||||||
|
|> assign(:edit_form, edit_form)}
|
||||||
|
end
|
||||||
|
|
||||||
|
And in the template:
|
||||||
|
|
||||||
|
<div id="messages" phx-update="stream">
|
||||||
|
<div :for={{id, message} <- @streams.messages} id={id} class="flex group">
|
||||||
|
{message.username}
|
||||||
|
<%= if @editing_message_id == message.id do %>
|
||||||
|
<%!-- Edit mode --%>
|
||||||
|
<.form for={@edit_form} id="edit-form-#{message.id}" phx-submit="save_edit">
|
||||||
|
...
|
||||||
|
</.form>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
- **Never** use the deprecated `phx-update="append"` or `phx-update="prepend"` for collections
|
||||||
|
|
||||||
|
### LiveView JavaScript interop
|
||||||
|
|
||||||
|
- Remember anytime you use `phx-hook="MyHook"` and that JS hook manages its own DOM, you **must** also set the `phx-update="ignore"` attribute
|
||||||
|
- **Always** provide an unique DOM id alongside `phx-hook` otherwise a compiler error will be raised
|
||||||
|
|
||||||
|
LiveView hooks come in two flavors, 1) colocated js hooks for "inline" scripts defined inside HEEx,
|
||||||
|
and 2) external `phx-hook` annotations where JavaScript object literals are defined and passed to the `LiveSocket` constructor.
|
||||||
|
|
||||||
|
#### Inline colocated js hooks
|
||||||
|
|
||||||
|
**Never** write raw embedded `<script>` tags in heex as they are incompatible with LiveView.
|
||||||
|
Instead, **always use a colocated js hook script tag (`:type={Phoenix.LiveView.ColocatedHook}`)
|
||||||
|
when writing scripts inside the template**:
|
||||||
|
|
||||||
|
<input type="text" name="user[phone_number]" id="user-phone-number" phx-hook=".PhoneNumber" />
|
||||||
|
<script :type={Phoenix.LiveView.ColocatedHook} name=".PhoneNumber">
|
||||||
|
export default {
|
||||||
|
mounted() {
|
||||||
|
this.el.addEventListener("input", e => {
|
||||||
|
let match = this.el.value.replace(/\D/g, "").match(/^(\d{3})(\d{3})(\d{4})$/)
|
||||||
|
if(match) {
|
||||||
|
this.el.value = `${match[1]}-${match[2]}-${match[3]}`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
- colocated hooks are automatically integrated into the app.js bundle
|
||||||
|
- colocated hooks names **MUST ALWAYS** start with a `.` prefix, i.e. `.PhoneNumber`
|
||||||
|
|
||||||
|
#### External phx-hook
|
||||||
|
|
||||||
|
External JS hooks (`<div id="myhook" phx-hook="MyHook">`) must be placed in `assets/js/` and passed to the
|
||||||
|
LiveSocket constructor:
|
||||||
|
|
||||||
|
const MyHook = {
|
||||||
|
mounted() { ... }
|
||||||
|
}
|
||||||
|
let liveSocket = new LiveSocket("/live", Socket, {
|
||||||
|
hooks: { MyHook }
|
||||||
|
});
|
||||||
|
|
||||||
|
#### Pushing events between client and server
|
||||||
|
|
||||||
|
Use LiveView's `push_event/3` when you need to push events/data to the client for a phx-hook to handle.
|
||||||
|
**Always** return or rebind the socket on `push_event/3` when pushing events:
|
||||||
|
|
||||||
|
# re-bind socket so we maintain event state to be pushed
|
||||||
|
socket = push_event(socket, "my_event", %{...})
|
||||||
|
|
||||||
|
# or return the modified socket directly:
|
||||||
|
def handle_event("some_event", _, socket) do
|
||||||
|
{:noreply, push_event(socket, "my_event", %{...})}
|
||||||
|
end
|
||||||
|
|
||||||
|
Pushed events can then be picked up in a JS hook with `this.handleEvent`:
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.handleEvent("my_event", data => console.log("from server:", data));
|
||||||
|
}
|
||||||
|
|
||||||
|
Clients can also push an event to the server and receive a reply with `this.pushEvent`:
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.el.addEventListener("click", e => {
|
||||||
|
this.pushEvent("my_event", { one: 1 }, reply => console.log("got reply from server:", reply));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Where the server handled it via:
|
||||||
|
|
||||||
|
def handle_event("my_event", %{"one" => 1}, socket) do
|
||||||
|
{:reply, %{two: 2}, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
### LiveView tests
|
||||||
|
|
||||||
|
- `Phoenix.LiveViewTest` module and `LazyHTML` (included) for making your assertions
|
||||||
|
- Form tests are driven by `Phoenix.LiveViewTest`'s `render_submit/2` and `render_change/2` functions
|
||||||
|
- Come up with a step-by-step test plan that splits major test cases into small, isolated files. You may start with simpler tests that verify content exists, gradually add interaction tests
|
||||||
|
- **Always reference the key element IDs you added in the LiveView templates in your tests** for `Phoenix.LiveViewTest` functions like `element/2`, `has_element/2`, selectors, etc
|
||||||
|
- **Never** tests again raw HTML, **always** use `element/2`, `has_element/2`, and similar: `assert has_element?(view, "#my-form")`
|
||||||
|
- Instead of relying on testing text content, which can change, favor testing for the presence of key elements
|
||||||
|
- Focus on testing outcomes rather than implementation details
|
||||||
|
- Be aware that `Phoenix.Component` functions like `<.form>` might produce different HTML than expected. Test against the output HTML structure, not your mental model of what you expect it to be
|
||||||
|
- When facing test failures with element selectors, add debug statements to print the actual HTML, but use `LazyHTML` selectors to limit the output, ie:
|
||||||
|
|
||||||
|
html = render(view)
|
||||||
|
document = LazyHTML.from_fragment(html)
|
||||||
|
matches = LazyHTML.filter(document, "your-complex-selector")
|
||||||
|
IO.inspect(matches, label: "Matches")
|
||||||
|
|
||||||
|
### Form handling
|
||||||
|
|
||||||
|
#### Creating a form from params
|
||||||
|
|
||||||
|
If you want to create a form based on `handle_event` params:
|
||||||
|
|
||||||
|
def handle_event("submitted", params, socket) do
|
||||||
|
{:noreply, assign(socket, form: to_form(params))}
|
||||||
|
end
|
||||||
|
|
||||||
|
When you pass a map to `to_form/1`, it assumes said map contains the form params, which are expected to have string keys.
|
||||||
|
|
||||||
|
You can also specify a name to nest the params:
|
||||||
|
|
||||||
|
def handle_event("submitted", %{"user" => user_params}, socket) do
|
||||||
|
{:noreply, assign(socket, form: to_form(user_params, as: :user))}
|
||||||
|
end
|
||||||
|
|
||||||
|
#### Creating a form from changesets
|
||||||
|
|
||||||
|
When using changesets, the underlying data, form params, and errors are retrieved from it. The `:as` option is automatically computed too. E.g. if you have a user schema:
|
||||||
|
|
||||||
|
defmodule MyApp.Users.User do
|
||||||
|
use Ecto.Schema
|
||||||
|
...
|
||||||
|
end
|
||||||
|
|
||||||
|
And then you create a changeset that you pass to `to_form`:
|
||||||
|
|
||||||
|
%MyApp.Users.User{}
|
||||||
|
|> Ecto.Changeset.change()
|
||||||
|
|> to_form()
|
||||||
|
|
||||||
|
Once the form is submitted, the params will be available under `%{"user" => user_params}`.
|
||||||
|
|
||||||
|
In the template, the form form assign can be passed to the `<.form>` function component:
|
||||||
|
|
||||||
|
<.form for={@form} id="todo-form" phx-change="validate" phx-submit="save">
|
||||||
|
<.input field={@form[:field]} type="text" />
|
||||||
|
</.form>
|
||||||
|
|
||||||
|
Always give the form an explicit, unique DOM ID, like `id="todo-form"`.
|
||||||
|
|
||||||
|
#### Avoiding form errors
|
||||||
|
|
||||||
|
**Always** use a form assigned via `to_form/2` in the LiveView, and the `<.input>` component in the template. In the template **always access forms this**:
|
||||||
|
|
||||||
|
<%!-- ALWAYS do this (valid) --%>
|
||||||
|
<.form for={@form} id="my-form">
|
||||||
|
<.input field={@form[:field]} type="text" />
|
||||||
|
</.form>
|
||||||
|
|
||||||
|
And **never** do this:
|
||||||
|
|
||||||
|
<%!-- NEVER do this (invalid) --%>
|
||||||
|
<.form for={@changeset} id="my-form">
|
||||||
|
<.input field={@changeset[:field]} type="text" />
|
||||||
|
</.form>
|
||||||
|
|
||||||
|
- You are FORBIDDEN from accessing the changeset in the template as it will cause errors
|
||||||
|
- **Never** use `<.form let={f} ...>` in the template, instead **always use `<.form for={@form} ...>`**, then drive all form references from the form assign as in `@form[:field]`. The UI should **always** be driven by a `to_form/2` assigned in the LiveView module that is derived from a changeset
|
||||||
|
|
||||||
|
<!-- phoenix:liveview-end -->
|
||||||
|
<!-- phoenix:phoenix-start -->
|
||||||
|
## phoenix:phoenix usage
|
||||||
|
## Phoenix guidelines
|
||||||
|
|
||||||
|
- Remember Phoenix router `scope` blocks include an optional alias which is prefixed for all routes within the scope. **Always** be mindful of this when creating routes within a scope to avoid duplicate module prefixes.
|
||||||
|
|
||||||
|
- You **never** need to create your own `alias` for route definitions! The `scope` provides the alias, ie:
|
||||||
|
|
||||||
|
scope "/admin", AppWeb.Admin do
|
||||||
|
pipe_through :browser
|
||||||
|
|
||||||
|
live "/users", UserLive, :index
|
||||||
|
end
|
||||||
|
|
||||||
|
the UserLive route would point to the `AppWeb.Admin.UserLive` module
|
||||||
|
|
||||||
|
- `Phoenix.View` no longer is needed or included with Phoenix, don't use it
|
||||||
|
|
||||||
|
<!-- phoenix:phoenix-end -->
|
||||||
|
<!-- fluxon-start -->
|
||||||
|
## fluxon usage
|
||||||
|
_Fluxon UI Components_
|
||||||
|
|
||||||
|
[fluxon usage rules](deps/fluxon/usage-rules.md)
|
||||||
|
<!-- fluxon-end -->
|
||||||
|
<!-- usage_rules-start -->
|
||||||
|
## usage_rules usage
|
||||||
|
_A dev tool for Elixir projects to gather LLM usage rules from dependencies_
|
||||||
|
|
||||||
|
## Using Usage Rules
|
||||||
|
|
||||||
|
Many packages have usage rules, which you should *thoroughly* consult before taking any
|
||||||
|
action. These usage rules contain guidelines and rules *directly from the package authors*.
|
||||||
|
They are your best source of knowledge for making decisions.
|
||||||
|
|
||||||
|
## Modules & functions in the current app and dependencies
|
||||||
|
|
||||||
|
When looking for docs for modules & functions that are dependencies of the current project,
|
||||||
|
or for Elixir itself, use `mix usage_rules.docs`
|
||||||
|
|
||||||
|
```
|
||||||
|
# Search a whole module
|
||||||
|
mix usage_rules.docs Enum
|
||||||
|
|
||||||
|
# Search a specific function
|
||||||
|
mix usage_rules.docs Enum.zip
|
||||||
|
|
||||||
|
# Search a specific function & arity
|
||||||
|
mix usage_rules.docs Enum.zip/1
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Searching Documentation
|
||||||
|
|
||||||
|
You should also consult the documentation of any tools you are using, early and often. The best
|
||||||
|
way to accomplish this is to use the `usage_rules.search_docs` mix task. Once you have
|
||||||
|
found what you are looking for, use the links in the search results to get more detail. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Search docs for all packages in the current application, including Elixir
|
||||||
|
mix usage_rules.search_docs Enum.zip
|
||||||
|
|
||||||
|
# Search docs for specific packages
|
||||||
|
mix usage_rules.search_docs Req.get -p req
|
||||||
|
|
||||||
|
# Search docs for multi-word queries
|
||||||
|
mix usage_rules.search_docs "making requests" -p req
|
||||||
|
|
||||||
|
# Search only in titles (useful for finding specific functions/modules)
|
||||||
|
mix usage_rules.search_docs "Enum.zip" --query-by title
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
<!-- usage_rules-end -->
|
||||||
|
<!-- usage_rules:elixir-start -->
|
||||||
|
## usage_rules:elixir usage
|
||||||
|
# Elixir Core Usage Rules
|
||||||
|
|
||||||
|
## Pattern Matching
|
||||||
|
- Use pattern matching over conditional logic when possible
|
||||||
|
- Prefer to match on function heads instead of using `if`/`else` or `case` in function bodies
|
||||||
|
- `%{}` matches ANY map, not just empty maps. Use `map_size(map) == 0` guard to check for truly empty maps
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
- Use `{:ok, result}` and `{:error, reason}` tuples for operations that can fail
|
||||||
|
- Avoid raising exceptions for control flow
|
||||||
|
- Use `with` for chaining operations that return `{:ok, _}` or `{:error, _}`
|
||||||
|
|
||||||
|
## Common Mistakes to Avoid
|
||||||
|
- Elixir has no `return` statement, nor early returns. The last expression in a block is always returned.
|
||||||
|
- Don't use `Enum` functions on large collections when `Stream` is more appropriate
|
||||||
|
- Avoid nested `case` statements - refactor to a single `case`, `with` or separate functions
|
||||||
|
- Don't use `String.to_atom/1` on user input (memory leak risk)
|
||||||
|
- Lists and enumerables cannot be indexed with brackets. Use pattern matching or `Enum` functions
|
||||||
|
- Prefer `Enum` functions like `Enum.reduce` over recursion
|
||||||
|
- When recursion is necessary, prefer to use pattern matching in function heads for base case detection
|
||||||
|
- Using the process dictionary is typically a sign of unidiomatic code
|
||||||
|
- Only use macros if explicitly requested
|
||||||
|
- There are many useful standard library functions, prefer to use them where possible
|
||||||
|
|
||||||
|
## Function Design
|
||||||
|
- Use guard clauses: `when is_binary(name) and byte_size(name) > 0`
|
||||||
|
- Prefer multiple function clauses over complex conditional logic
|
||||||
|
- Name functions descriptively: `calculate_total_price/2` not `calc/2`
|
||||||
|
- Predicate function names should not start with `is` and should end in a question mark.
|
||||||
|
- Names like `is_thing` should be reserved for guards
|
||||||
|
|
||||||
|
## Data Structures
|
||||||
|
- Use structs over maps when the shape is known: `defstruct [:name, :age]`
|
||||||
|
- Prefer keyword lists for options: `[timeout: 5000, retries: 3]`
|
||||||
|
- Use maps for dynamic key-value data
|
||||||
|
- Prefer to prepend to lists `[new | list]` not `list ++ [new]`
|
||||||
|
|
||||||
|
## Mix Tasks
|
||||||
|
|
||||||
|
- Use `mix help` to list available mix tasks
|
||||||
|
- Use `mix help task_name` to get docs for an individual task
|
||||||
|
- Read the docs and options fully before using tasks
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
- Run tests in a specific file with `mix test test/my_test.exs` and a specific test with the line number `mix test path/to/test.exs:123`
|
||||||
|
- Limit the number of failed tests with `mix test --max-failures n`
|
||||||
|
- Use `@tag` to tag specific tests, and `mix test --only tag` to run only those tests
|
||||||
|
- Use `assert_raise` for testing expected exceptions: `assert_raise ArgumentError, fn -> invalid_function() end`
|
||||||
|
- Use `mix help test` to for full documentation on running tests
|
||||||
|
|
||||||
|
## Debugging
|
||||||
|
|
||||||
|
- Use `dbg/1` to print values while debugging. This will display the formatted value and other relevant information in the console.
|
||||||
|
|
||||||
|
<!-- usage_rules:elixir-end -->
|
||||||
|
<!-- usage_rules:otp-start -->
|
||||||
|
## usage_rules:otp usage
|
||||||
|
# OTP Usage Rules
|
||||||
|
|
||||||
|
## GenServer Best Practices
|
||||||
|
- Keep state simple and serializable
|
||||||
|
- Handle all expected messages explicitly
|
||||||
|
- Use `handle_continue/2` for post-init work
|
||||||
|
- Implement proper cleanup in `terminate/2` when necessary
|
||||||
|
|
||||||
|
## Process Communication
|
||||||
|
- Use `GenServer.call/3` for synchronous requests expecting replies
|
||||||
|
- Use `GenServer.cast/2` for fire-and-forget messages.
|
||||||
|
- When in doubt, use `call` over `cast`, to ensure back-pressure
|
||||||
|
- Set appropriate timeouts for `call/3` operations
|
||||||
|
|
||||||
|
## Fault Tolerance
|
||||||
|
- Set up processes such that they can handle crashing and being restarted by supervisors
|
||||||
|
- Use `:max_restarts` and `:max_seconds` to prevent restart loops
|
||||||
|
|
||||||
|
## Task and Async
|
||||||
|
- Use `Task.Supervisor` for better fault tolerance
|
||||||
|
- Handle task failures with `Task.yield/2` or `Task.shutdown/2`
|
||||||
|
- Set appropriate task timeouts
|
||||||
|
- Use `Task.async_stream/3` for concurrent enumeration with back-pressure
|
||||||
|
|
||||||
|
<!-- usage_rules:otp-end -->
|
||||||
|
<!-- usage-rules-end -->
|
||||||
@@ -82,6 +82,7 @@ defmodule MusicLibrary.MixProject do
|
|||||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||||
{:quokka, "~> 2.6", only: [:dev, :test], runtime: false},
|
{:quokka, "~> 2.6", only: [:dev, :test], runtime: false},
|
||||||
{:live_debugger, "~> 0.5.0", only: :dev},
|
{:live_debugger, "~> 0.5.0", only: :dev},
|
||||||
|
{:usage_rules, "~> 0.1"},
|
||||||
|
|
||||||
# HTTP Clients
|
# HTTP Clients
|
||||||
{:finch, "~> 0.21.0"},
|
{:finch, "~> 0.21.0"},
|
||||||
@@ -141,7 +142,8 @@ defmodule MusicLibrary.MixProject do
|
|||||||
"ecto.setup",
|
"ecto.setup",
|
||||||
"assets.setup",
|
"assets.setup",
|
||||||
"cmd npm ci --prefix assets",
|
"cmd npm ci --prefix assets",
|
||||||
"assets.build"
|
"assets.build",
|
||||||
|
"usage_rules.sync AGENTS.md --all --inline usage_rules:all --link-to-folder deps --remove-missing --yes"
|
||||||
],
|
],
|
||||||
"ecto.setup": ["ecto.create", "ecto.migrate"],
|
"ecto.setup": ["ecto.create", "ecto.migrate"],
|
||||||
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
||||||
|
|||||||
@@ -23,8 +23,10 @@
|
|||||||
"flagmojis": {:hex, :flagmojis, "1.0.0", "d2b411d9d2c150180103a4d605ee394cabc23c2b6ddf7a0011a9a55a23575281", [:mix], [], "hexpm", "9d9916575cf9dcbe6f0199b410994bec9d19d47971f70089eb7f210f823cd293"},
|
"flagmojis": {:hex, :flagmojis, "1.0.0", "d2b411d9d2c150180103a4d605ee394cabc23c2b6ddf7a0011a9a55a23575281", [:mix], [], "hexpm", "9d9916575cf9dcbe6f0199b410994bec9d19d47971f70089eb7f210f823cd293"},
|
||||||
"fluxon": {:hex, :fluxon, "2.3.1", "af66ad3a45ca802d983fe4483acc4c2a23b48c1703d743ca9e8d00191370dc1d", [:mix], [{:phoenix_live_view, ">= 1.0.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "fluxon", "dcc91bcbce390e6633a576e0338ca46d4abc1f291f785337e8f62cbb87d1be49"},
|
"fluxon": {:hex, :fluxon, "2.3.1", "af66ad3a45ca802d983fe4483acc4c2a23b48c1703d743ca9e8d00191370dc1d", [:mix], [{:phoenix_live_view, ">= 1.0.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "fluxon", "dcc91bcbce390e6633a576e0338ca46d4abc1f291f785337e8f62cbb87d1be49"},
|
||||||
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
|
"gettext": {:hex, :gettext, "1.0.2", "5457e1fd3f4abe47b0e13ff85086aabae760497a3497909b8473e0acee57673b", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "eab805501886802071ad290714515c8c4a17196ea76e5afc9d06ca85fb1bfeb3"},
|
||||||
|
"glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"},
|
||||||
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
|
"heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "0435d4ca364a608cc75e2f8683d374e55abbae26", [tag: "v2.2.0", sparse: "optimized", depth: 1]},
|
||||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||||
|
"igniter": {:hex, :igniter, "0.7.2", "81c132c0df95963c7a228f74a32d3348773743ed9651f24183bfce0fe6ff16d1", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "f4cab73ec31f4fb452de1a17037f8a08826105265aa2d76486fcb848189bef9b"},
|
||||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||||
"lazy_html": {:hex, :lazy_html, "0.1.8", "677a8642e644eef8de98f3040e2520d42d0f0f8bd6c5cd49db36504e34dffe91", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "0d8167d930b704feb94b41414ca7f5779dff9bca7fcf619fcef18de138f08736"},
|
"lazy_html": {:hex, :lazy_html, "0.1.8", "677a8642e644eef8de98f3040e2520d42d0f0f8bd6c5cd49db36504e34dffe91", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.9.0", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:fine, "~> 0.1.0", [hex: :fine, repo: "hexpm", optional: false]}], "hexpm", "0d8167d930b704feb94b41414ca7f5779dff9bca7fcf619fcef18de138f08736"},
|
||||||
"live_debugger": {:hex, :live_debugger, "0.5.1", "7302a4fda1920ba541b456c2d7a97acc3c7f9d7b938b5435927883b709c968a2", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.8 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "797fdca7cc60d7588c6e285b0d7ea73f2dce8b123bac43eae70271fa519bb907"},
|
"live_debugger": {:hex, :live_debugger, "0.5.1", "7302a4fda1920ba541b456c2d7a97acc3c7f9d7b938b5435927883b709c968a2", [:mix], [{:igniter, ">= 0.5.40 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.8 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "797fdca7cc60d7588c6e285b0d7ea73f2dce8b123bac43eae70271fa519bb907"},
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
"oban": {:hex, :oban, "2.20.3", "e4d27336941955886cc7113420c32c63b70b64f10b27e08e3cf2b001153953cd", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "075ffbf1279a96bec495bc63d647b08929837d70bcc0427249ffe4d1dddaec33"},
|
"oban": {:hex, :oban, "2.20.3", "e4d27336941955886cc7113420c32c63b70b64f10b27e08e3cf2b001153953cd", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.20", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.3", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "075ffbf1279a96bec495bc63d647b08929837d70bcc0427249ffe4d1dddaec33"},
|
||||||
"oban_met": {:hex, :oban_met, "1.0.5", "bb633ab06448dab2ef9194f6688d33b3d07fc3f2ad793a1a08f4dfbb2cc9fe50", [:mix], [{:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "64664d50805bbfd3903aeada1f3c39634652a87844797ee400b0bcc95a28f5ea"},
|
"oban_met": {:hex, :oban_met, "1.0.5", "bb633ab06448dab2ef9194f6688d33b3d07fc3f2ad793a1a08f4dfbb2cc9fe50", [:mix], [{:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}], "hexpm", "64664d50805bbfd3903aeada1f3c39634652a87844797ee400b0bcc95a28f5ea"},
|
||||||
"oban_web": {:hex, :oban_web, "2.11.7", "a998868bb32b3d3c44f328a8baec820af5c3b46a3a743d89c169e75b0bf7efcc", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_met, "~> 1.0", [hex: :oban_met, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "f0ef9ec8f97381a0287c60a732736ff77bc351cbbe82eceffb5a635f84788656"},
|
"oban_web": {:hex, :oban_web, "2.11.7", "a998868bb32b3d3c44f328a8baec820af5c3b46a3a743d89c169e75b0bf7efcc", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:oban, "~> 2.19", [hex: :oban, repo: "hexpm", optional: false]}, {:oban_met, "~> 1.0", [hex: :oban_met, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.7", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "f0ef9ec8f97381a0287c60a732736ff77bc351cbbe82eceffb5a635f84788656"},
|
||||||
|
"owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"},
|
||||||
"phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, 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.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"},
|
"phoenix": {:hex, :phoenix, "1.8.3", "49ac5e485083cb1495a905e47eb554277bdd9c65ccb4fc5100306b350151aa95", [:mix], [{:bandit, "~> 1.0", [hex: :bandit, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, 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.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.7", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "36169f95cc2e155b78be93d9590acc3f462f1e5438db06e6248613f27c80caec"},
|
||||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [: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", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
|
"phoenix_ecto": {:hex, :phoenix_ecto, "4.7.0", "75c4b9dfb3efdc42aec2bd5f8bccd978aca0651dbcbc7a3f362ea5d9d43153c6", [: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", "1d75011e4254cb4ddf823e81823a9629559a1be93b4321a6a5f11a5306fbf4cc"},
|
||||||
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
||||||
@@ -53,14 +56,19 @@
|
|||||||
"quokka": {:hex, :quokka, "2.11.2", "2856118154425f18547720d997199be54febed771a740ba3c988a17762328287", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "8208f5d814007cb35a2eb278462464d083fca8c463f62517ab94eef982f181cc"},
|
"quokka": {:hex, :quokka, "2.11.2", "2856118154425f18547720d997199be54febed771a740ba3c988a17762328287", [:mix], [{:credo, "~> 1.7", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "8208f5d814007cb35a2eb278462464d083fca8c463f62517ab94eef982f181cc"},
|
||||||
"recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
|
"recon": {:hex, :recon, "2.5.6", "9052588e83bfedfd9b72e1034532aee2a5369d9d9343b61aeb7fbce761010741", [:mix, :rebar3], [], "hexpm", "96c6799792d735cc0f0fd0f86267e9d351e63339cbe03df9d162010cefc26bb0"},
|
||||||
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [: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", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
|
"req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [: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", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"},
|
||||||
|
"rewrite": {:hex, :rewrite, "1.2.0", "80220eb14010e175b67c939397e1a8cdaa2c32db6e2e0a9d5e23e45c0414ce21", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "a1cd702bbb9d51613ab21091f04a386d750fc6f4516b81900df082d78b2d8c50"},
|
||||||
"sentry": {:hex, :sentry, "11.0.4", "60371c96cefd247e0fc98840bba2648f64f19aa0b8db8e938f5a98421f55b619", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_ownership, "~> 0.3.0 or ~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:opentelemetry, ">= 0.0.0", [hex: :opentelemetry, repo: "hexpm", optional: true]}, {:opentelemetry_api, ">= 0.0.0", [hex: :opentelemetry_api, repo: "hexpm", optional: true]}, {:opentelemetry_exporter, ">= 0.0.0", [hex: :opentelemetry_exporter, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, ">= 0.0.0", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "feaafc284dc204c82aadaddc884227aeaa3480decb274d30e184b9d41a700c66"},
|
"sentry": {:hex, :sentry, "11.0.4", "60371c96cefd247e0fc98840bba2648f64f19aa0b8db8e938f5a98421f55b619", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_ownership, "~> 0.3.0 or ~> 1.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:opentelemetry, ">= 0.0.0", [hex: :opentelemetry, repo: "hexpm", optional: true]}, {:opentelemetry_api, ">= 0.0.0", [hex: :opentelemetry_api, repo: "hexpm", optional: true]}, {:opentelemetry_exporter, ">= 0.0.0", [hex: :opentelemetry_exporter, repo: "hexpm", optional: true]}, {:opentelemetry_semantic_conventions, ">= 0.0.0", [hex: :opentelemetry_semantic_conventions, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: true]}, {:phoenix_live_view, "~> 0.20 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "feaafc284dc204c82aadaddc884227aeaa3480decb274d30e184b9d41a700c66"},
|
||||||
|
"sourceror": {:hex, :sourceror, "1.10.1", "325753ed460fe9fa34ebb4deda76d57b2e1507dcd78a5eb9e1c41bfb78b7cdfe", [:mix], [], "hexpm", "288f3079d93865cd1e3e20df5b884ef2cb440e0e03e8ae393624ee8a770ba588"},
|
||||||
|
"spitfire": {:hex, :spitfire, "0.3.2", "476b7b5151fd053a864dae7b5eaeed01811e8b2ff3f24f3c048af1c9dfee5e3d", [:mix], [], "hexpm", "014f7b8c6dd45d1e3b08103c7e61515a590efc872441cf3e933a20efa4b5c46c"},
|
||||||
"table_rex": {:hex, :table_rex, "4.1.0", "fbaa8b1ce154c9772012bf445bfb86b587430fb96f3b12022d3f35ee4a68c918", [:mix], [], "hexpm", "95932701df195d43bc2d1c6531178fc8338aa8f38c80f098504d529c43bc2601"},
|
"table_rex": {:hex, :table_rex, "4.1.0", "fbaa8b1ce154c9772012bf445bfb86b587430fb96f3b12022d3f35ee4a68c918", [:mix], [], "hexpm", "95932701df195d43bc2d1c6531178fc8338aa8f38c80f098504d529c43bc2601"},
|
||||||
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
|
"tailwind": {:hex, :tailwind, "0.4.1", "e7bcc222fe96a1e55f948e76d13dd84a1a7653fb051d2a167135db3b4b08d3e9", [:mix], [], "hexpm", "6249d4f9819052911120dbdbe9e532e6bd64ea23476056adb7f730aa25c220d1"},
|
||||||
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
|
||||||
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
"telemetry_metrics": {:hex, :telemetry_metrics, "1.1.0", "5bd5f3b5637e0abea0426b947e3ce5dd304f8b3bc6617039e2b5a008adc02f8f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e7b79e8ddfde70adb6db8a6623d1778ec66401f366e9a8f5dd0955c56bc8ce67"},
|
||||||
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
|
"telemetry_poller": {:hex, :telemetry_poller, "1.3.0", "d5c46420126b5ac2d72bc6580fb4f537d35e851cc0f8dbd571acf6d6e10f5ec7", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "51f18bed7128544a50f75897db9974436ea9bfba560420b646af27a9a9b35211"},
|
||||||
|
"text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"},
|
||||||
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
|
"thousand_island": {:hex, :thousand_island, "1.4.3", "2158209580f633be38d43ec4e3ce0a01079592b9657afff9080d5d8ca149a3af", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6e4ce09b0fd761a58594d02814d40f77daff460c48a7354a15ab353bb998ea0b"},
|
||||||
"time_zone_info": {:hex, :time_zone_info, "0.7.10", "79fdfd16e70bb2e514ceff8bd81af4f0b5f46d45c8e5f6485de5ad29f8d4252a", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "abe6d744ff152788a36c2c2fce6cd007ae031c6a4c540f8d3d325f24a775aeba"},
|
"time_zone_info": {:hex, :time_zone_info, "0.7.10", "79fdfd16e70bb2e514ceff8bd81af4f0b5f46d45c8e5f6485de5ad29f8d4252a", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "abe6d744ff152788a36c2c2fce6cd007ae031c6a4c540f8d3d325f24a775aeba"},
|
||||||
|
"usage_rules": {:hex, :usage_rules, "0.1.26", "19d38c8b9b5c35434eae44f7e4554caeb5f08037a1d45a6b059a9782543ac22e", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "9f0d203aa288e1b48318929066778ec26fc423fd51f08518c5b47f58ad5caca9"},
|
||||||
"vix": {:hex, :vix, "0.37.0", "238e77280368790ed727c7b30e27d237a00360f81bdaaeb2070f048dd837bb06", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "8f64d453a65942ab6cf71f1128dbebe724873c94801fe9025d28893947ceeb01"},
|
"vix": {:hex, :vix, "0.37.0", "238e77280368790ed727c7b30e27d237a00360f81bdaaeb2070f048dd837bb06", [:make, :mix], [{:cc_precompiler, "~> 0.1.4 or ~> 0.2", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.7.3 or ~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}], "hexpm", "8f64d453a65942ab6cf71f1128dbebe724873c94801fe9025d28893947ceeb01"},
|
||||||
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
|
||||||
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [: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", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
|
"websock_adapter": {:hex, :websock_adapter, "0.5.9", "43dc3ba6d89ef5dec5b1d0a39698436a1e856d000d84bf31a3149862b01a287f", [: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", "5534d5c9adad3c18a0f58a9371220d75a803bf0b9a3d87e6fe072faaeed76a08"},
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ debug_msg "Updating mix dependencies"
|
|||||||
|
|
||||||
mix deps.update --all
|
mix deps.update --all
|
||||||
|
|
||||||
|
mix usage_rules.sync AGENTS.md --all \
|
||||||
|
--inline usage_rules:all \
|
||||||
|
--link-to-folder deps \
|
||||||
|
--remove-missing \
|
||||||
|
--yes
|
||||||
|
|
||||||
debug_msg "Updating gh extensions"
|
debug_msg "Updating gh extensions"
|
||||||
|
|
||||||
gh extension upgrade gfm-preview
|
gh extension upgrade gfm-preview
|
||||||
|
|||||||
Reference in New Issue
Block a user