You've already forked tribes-plugin-sender
forked from tribes/tribes-plugin-template
chore: Update plugin scaffolding
This commit is contained in:
@@ -0,0 +1,656 @@
|
||||
# Tribes Sender Plugin Agent Guide
|
||||
|
||||
This repository is the Tribes Sender plugin for RTMP ingest and HLS/LL-HLS
|
||||
streaming. Keep changes scoped to sender-owned runtime, migration, asset, and
|
||||
test surfaces.
|
||||
|
||||
## Required Workflow
|
||||
|
||||
- Use `scripts/plugin` for plugin-aware commands: `scripts/plugin validate`,
|
||||
`scripts/plugin test`, `scripts/plugin precommit`, and `scripts/plugin smoke`.
|
||||
- Use `devenv shell -- <command>` when running repo commands from outside the
|
||||
devenv shell.
|
||||
- Do not bypass the plugin wrapper for host-backed validation.
|
||||
|
||||
## Plugin Contract
|
||||
|
||||
- `manifest.json` `entry_module` must point at `Tribes.Plugins.Sender.Plugin`.
|
||||
- Keep streaming supervision children restartable under normal OTP supervision.
|
||||
- Keep runtime spec fields aligned with `manifest.json`.
|
||||
|
||||
|
||||
<!-- usage-rules-start -->
|
||||
<!-- usage_rules-start -->
|
||||
## usage_rules usage
|
||||
_A config-driven dev tool for Elixir projects to manage AGENTS.md files and agent skills 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 -->
|
||||
<!-- 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 programmatically, 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: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} <- @streams.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 -->
|
||||
<!-- ash-start -->
|
||||
## ash usage
|
||||
_A declarative, extensible framework for building Elixir applications._
|
||||
|
||||
# Rules for working with Ash
|
||||
|
||||
## Understanding Ash
|
||||
|
||||
Ash is an opinionated, composable framework for building applications in Elixir. It provides a declarative approach to modeling your domain with resources at the center. Read documentation *before* attempting to use its features. Do not assume that you have prior knowledge of the framework or its conventions.
|
||||
|
||||
|
||||
<!-- ash-end -->
|
||||
<!-- ash:actions-start -->
|
||||
## ash:actions usage
|
||||
[ash:actions usage rules](deps/ash/usage-rules/actions.md)
|
||||
<!-- ash:actions-end -->
|
||||
<!-- ash:migrations-start -->
|
||||
## ash:migrations usage
|
||||
[ash:migrations usage rules](deps/ash/usage-rules/migrations.md)
|
||||
<!-- ash:migrations-end -->
|
||||
<!-- ash:testing-start -->
|
||||
## ash:testing usage
|
||||
[ash:testing usage rules](deps/ash/usage-rules/testing.md)
|
||||
<!-- ash:testing-end -->
|
||||
<!-- tribes_plugin_api-start -->
|
||||
## tribes_plugin_api usage
|
||||
_tribes_plugin_api_
|
||||
|
||||
# Tribes Plugin API Usage Rules
|
||||
|
||||
These rules apply when implementing the public plugin contract from
|
||||
`tribes_plugin_api`. For deeper context from a plugin checkout, see
|
||||
[`../tribes/docs/plugins.md`](../tribes/docs/plugins.md); from this package
|
||||
source directory, the same document is at `../docs/plugins.md`.
|
||||
|
||||
## Entry Modules
|
||||
|
||||
- Implement the runtime contract with `Tribes.Plugin` or `Tribes.Plugin.Base`.
|
||||
- Prefer `use Tribes.Plugin.Base, otp_app: :your_plugin` for normal plugins; it
|
||||
reads `manifest.json` and fills the manifest-backed spec fields.
|
||||
- Keep the host-facing entry module under `Tribes.Plugins.*.Plugin`. It may be a
|
||||
thin delegate to the plugin application's own module.
|
||||
- `register/1` must return a `Tribes.Plugin.Spec` struct or a map/struct that
|
||||
validates into that spec.
|
||||
|
||||
## Spec Discipline
|
||||
|
||||
- Fields mirrored from `manifest.json` must match exactly after capability
|
||||
normalization: `name`, `version`, `provider_priority`, `provides`,
|
||||
`requires`, and `enhances_with`.
|
||||
- Use `%Tribes.Plugin.Spec.NavItem{}` and `%Tribes.Plugin.Spec.Page{}` or maps
|
||||
with only the documented keys. Unknown keys fail validation.
|
||||
- Use atom modules for `live_view`, plug modules, hook modules, `ui_components`,
|
||||
and `ash_domains`; do not pass module names as strings in the runtime spec.
|
||||
- Keep `children` as valid supervisor child specs and make plugin processes
|
||||
restartable under normal OTP supervision rules.
|
||||
- Run `scripts/plugin validate` or `mix tribes.plugin.validate` before relying
|
||||
on runtime registration behavior.
|
||||
|
||||
## Pages, Layouts, And Auth
|
||||
|
||||
- Use `Tribes.Plugin.Layouts.app` for pages that should render inside host
|
||||
chrome.
|
||||
- Use `Tribes.Plugin.LiveUserAuth` on plugin LiveViews when they need host user
|
||||
context.
|
||||
- Avoid `use TribesWeb, :live_view` in standalone release-facing plugin code;
|
||||
use `Phoenix.LiveView` and public plugin/UI APIs instead.
|
||||
|
||||
## Config Schema
|
||||
|
||||
- `config_schema` is for small admin-editable runtime defaults rendered by the
|
||||
host UI and stored through `Tribes.ConfigStore`.
|
||||
- Group IDs and setting keys must use the identifier format accepted by the
|
||||
validator: lowercase segments with letters, digits, underscores, and dots.
|
||||
- Supported setting types are `:string`, `:text`, `:boolean`, `:integer`,
|
||||
`:number`, `:enum`, `:list`, and `:object`.
|
||||
- Use `options` only with `:enum`, and provide stable stored values rather than
|
||||
display text as the value.
|
||||
|
||||
<!-- tribes_plugin_api-end -->
|
||||
<!-- tribes-start -->
|
||||
## tribes usage
|
||||
_tribes_
|
||||
|
||||
# Tribes Plugin Development Rules
|
||||
|
||||
These rules are for external Tribes plugin projects that depend on the host
|
||||
checkout during development. For the full contract, see
|
||||
[`../tribes/docs/plugins.md`](../tribes/docs/plugins.md) and
|
||||
[`../tribes/docs/ui.md`](../tribes/docs/ui.md).
|
||||
|
||||
## Plugin Boundaries
|
||||
|
||||
- Treat plugins as separate OTP applications. The host discovers them through
|
||||
`manifest.json` and a single `Tribes.Plugin` entry module, not by reaching
|
||||
into plugin internals.
|
||||
- Keep plugin contributions inside the supported runtime spec fields:
|
||||
`nav_items`, `pages`, `api_routes`, `plugs`, `children`, `global_js`,
|
||||
`global_css`, `migrations_path`, `ui_components`, `hooks`, `ash_domains`, and
|
||||
`config_schema`.
|
||||
- Do not mutate host routers, host Ash domains, endpoint config, or host
|
||||
supervision trees directly from plugin code.
|
||||
- Plugin-owned pages and API routes should live under plugin-owned paths. Avoid
|
||||
taking over unrelated host sections.
|
||||
|
||||
## Manifest And Runtime Spec
|
||||
|
||||
- `manifest.json` is the static build/runtime contract. Keep `name`,
|
||||
`version`, `entry_module`, `host_api`, `otp_app`, `provides`, `requires`, and
|
||||
`enhances_with` aligned with the runtime spec returned by `register/1`.
|
||||
- `entry_module` must be under `Tribes.Plugins.*.Plugin` and end in `.Plugin`.
|
||||
- Capability versions are discrete breaking-change markers such as `ui@1` or
|
||||
`ecto@1`, not semver.
|
||||
- Use `requires` for hard dependencies and `enhances_with` for optional
|
||||
integrations that the plugin can run without.
|
||||
- Run `scripts/plugin validate` after changing `manifest.json` or the runtime
|
||||
plugin spec.
|
||||
|
||||
## UI And Assets
|
||||
|
||||
- Plugin LiveViews that use host chrome should render with
|
||||
`Tribes.Plugin.Layouts.app` and keep `ui@1` in `manifest.json` `requires`.
|
||||
- Consumers should target the `ui@1` facade with `use Tribes.UI` or
|
||||
`import Tribes.UI.Components`, not a concrete provider module.
|
||||
- Declare browser assets in `manifest.json` under `assets.global_js` and
|
||||
`assets.global_css`; the host serves them through the plugin asset surface.
|
||||
- Keep CSS selectors scoped to the plugin, normally with a plugin-specific root
|
||||
class.
|
||||
|
||||
## Runtime Config
|
||||
|
||||
- Use plugin OTP app env only for boot-time wiring that is genuinely static.
|
||||
- For mutable runtime settings, use `Tribes.ConfigStore` and expose small
|
||||
editable defaults through `config_schema` in the plugin spec.
|
||||
- `config_schema` is validated by the host and rendered by Tribes in the admin
|
||||
settings UI. It describes fields only; values are stored as ConfigStore
|
||||
overrides when an admin saves them.
|
||||
- If `config_schema.namespace` is omitted, the host uses
|
||||
`plugin.<plugin_name>`.
|
||||
- Plugins should read ConfigStore values with the same defaults declared in the
|
||||
schema, because saving a value equal to the default deletes the override.
|
||||
- Do not expose secrets, node-local paths, ports, database URLs, TLS material,
|
||||
package selection, or deployment state through `config_schema`.
|
||||
|
||||
## Synced Data
|
||||
|
||||
- If a plugin adds Ash resources that should replicate across the cluster, add
|
||||
them deliberately to the plugin spec `ash_domains` and document replication
|
||||
semantics in the plugin contract docs.
|
||||
- Use `AshNostrSync` only for resources whose persisted state is meant to be
|
||||
cluster-synced. Do not enable it as a default on every plugin resource.
|
||||
|
||||
<!-- tribes-end -->
|
||||
<!-- usage-rules-end -->
|
||||
@@ -9,7 +9,8 @@ defmodule Sender.MixProject do
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps(),
|
||||
aliases: aliases()
|
||||
aliases: aliases(),
|
||||
usage_rules: usage_rules()
|
||||
]
|
||||
end
|
||||
|
||||
@@ -36,6 +37,10 @@ defmodule Sender.MixProject do
|
||||
# For CI or standalone development, this can be replaced with a published
|
||||
# package once tribes_plugin_api is released.
|
||||
{:tribes_plugin_api, path: "../tribes/tribes_plugin_api", runtime: false},
|
||||
{:tribes_plugin, path: "../tribes-plugin-new", only: [:dev, :test], runtime: false},
|
||||
{:igniter, "~> 0.7", only: [:dev, :test], runtime: false},
|
||||
{:ash, "~> 3.0", only: [:dev, :test], runtime: false},
|
||||
{:usage_rules, "~> 1.2", only: :dev},
|
||||
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||
{:lazy_html, ">= 0.1.0", only: :test},
|
||||
{:phoenix, "~> 1.8"},
|
||||
@@ -61,4 +66,25 @@ defmodule Sender.MixProject do
|
||||
]
|
||||
]
|
||||
end
|
||||
|
||||
defp usage_rules do
|
||||
[
|
||||
file: "AGENTS.md",
|
||||
usage_rules: [
|
||||
{:usage_rules, sub_rules: []},
|
||||
{"usage_rules:elixir", main: false},
|
||||
{"usage_rules:otp", main: false},
|
||||
"phoenix:ecto",
|
||||
"phoenix:html",
|
||||
"phoenix:liveview",
|
||||
"phoenix:phoenix",
|
||||
{:ash, sub_rules: []},
|
||||
{"ash:actions", link: :markdown, main: false},
|
||||
{"ash:migrations", link: :markdown, main: false},
|
||||
{"ash:testing", link: :markdown, main: false},
|
||||
{:tribes_plugin_api, sub_rules: []},
|
||||
{:tribes, sub_rules: []}
|
||||
]
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,8 +43,10 @@
|
||||
"finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [: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", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"},
|
||||
"fine": {:hex, :fine, "0.1.6", "4bf7151493443c454aac9f2fa2f34f5fefd0346a83fb5586a016c4a135c63247", [:mix], [], "hexpm", "5638eb4495488e885ebec167fa57973e5c35e1a50c344eb7666c90ec1c4e3b12"},
|
||||
"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"},
|
||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"igniter": {:hex, :igniter, "0.7.9", "8c573440b8127fd80be8220fb197e7422317a81072054fcc0b336029f035a416", [: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", "123513d09f3af149db851aad8492b5b49f861d2c466a72031b2a0cbd9f45526f"},
|
||||
"iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"joken": {:hex, :joken, "2.6.2", "5daaf82259ca603af4f0b065475099ada1b2b849ff140ccd37f4b6828ca6892a", [:mix], [{:jose, "~> 1.11.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "5134b5b0a6e37494e46dbf9e4dad53808e5e787904b7c73972651b51cce3d72b"},
|
||||
@@ -57,6 +59,7 @@
|
||||
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
|
||||
"nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"},
|
||||
"nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"},
|
||||
"owl": {:hex, :owl, "0.13.0", "26010e066d5992774268f3163506972ddac0a7e77bfe57fa42a250f24d6b876e", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "59bf9d11ce37a4db98f57cb68fbfd61593bf419ec4ed302852b6683d3d2f7475"},
|
||||
"parrhesia": {:hex, :parrhesia, "0.12.0", "67f33b62a6d7d32ce8d801e3dd09f3e8f46d82a21e9b15dfda6a161d2c7f7e09", [:mix], [{:bandit, "~> 1.5", [hex: :bandit, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:lib_secp256k1, "~> 0.7", [hex: :lib_secp256k1, repo: "hexpm", optional: false]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus, "~> 1.1", [hex: :telemetry_metrics_prometheus, repo: "hexpm", optional: false]}, {:telemetry_poller, "~> 1.0", [hex: :telemetry_poller, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5", [hex: :websock_adapter, repo: "hexpm", optional: false]}, {:websockex, "~> 0.4", [hex: :websockex, repo: "hexpm", optional: false]}], "hexpm", "e91f757972746c73f888706cb084ba745ebf1a602a0f2f6a7c380555c6066cc3"},
|
||||
"phoenix": {:hex, :phoenix, "1.8.5", "919db335247e6d4891764dc3063415b0d2457641c5f9b3751b5df03d8e20bbcf", [: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", "83b2bb125127e02e9f475c8e3e92736325b5b01b0b9b05407bcb4083b7a32485"},
|
||||
"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"},
|
||||
@@ -75,8 +78,11 @@
|
||||
"ranch": {:hex, :ranch, "2.2.0", "25528f82bc8d7c6152c57666ca99ec716510fe0925cb188172f41ce93117b1b0", [:make, :rebar3], [], "hexpm", "fa0b99a1780c80218a4197a59ea8d3bdae32fbff7e88527d7d8a4787eff4f8e7"},
|
||||
"reactor": {:hex, :reactor, "1.0.1", "ca3b5cf3c04ec8441e67ea2625d0294939822060b1bfd00ffdaaf75b7682d991", [:mix], [{:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:iterex, "~> 0.1", [hex: :iterex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:libgraph, "~> 0.16", [hex: :libgraph, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.3 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.2", [hex: :telemetry, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}, {:ymlr, "~> 5.0", [hex: :ymlr, repo: "hexpm", optional: false]}], "hexpm", "3497db2b204c9a3cabdaf1b26d2405df1dfbb138ce0ce50e616e9db19fec0043"},
|
||||
"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.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [: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", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"},
|
||||
"slugify": {:hex, :slugify, "1.3.1", "0d3b8b7e5c1eeaa960e44dce94382bee34a39b3ea239293e457a9c5b47cc6fd3", [:mix], [], "hexpm", "cb090bbeb056b312da3125e681d98933a360a70d327820e4b7f91645c4d8be76"},
|
||||
"sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"},
|
||||
"spark": {:hex, :spark, "2.6.1", "b0100216d3883c6a281cb2434af45afbd808695aadb034923cbaf7d8a2ba46ab", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "77bbefa5263bb6b70e1195bc0fc662ddb8ef5937a356a77ae072e56983ad13f0"},
|
||||
"spitfire": {:hex, :spitfire, "0.3.11", "79dfcb033762470de472c1c26ea2b4e3aca74700c685dbffd9a13466272c323d", [:mix], [], "hexpm", "eb6e2dadf63214e8bfe65ca9788cef2b03b01027365d78d3c0e3d9ebd3d5b7b4"},
|
||||
"splode": {:hex, :splode, "0.3.1", "9843c54f84f71b7833fec3f0be06c3cfb5be6b35960ee195ea4fad84b1c25030", [:mix], [], "hexpm", "8f2309b6ec2ecbb01435656429ed1d9ed04ba28797a3280c3b0d1217018ecfbd"},
|
||||
"stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"},
|
||||
"swoosh": {:hex, :swoosh, "1.25.0", "d60dcba6d1ce538b1994f8712a3d55bc9519ffba4654cc4665a75683881d11dd", [: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]}, {:idna, "~> 6.0", [hex: :idna, repo: "hexpm", optional: false]}, {: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.10 or ~> 0.6 or ~> 1.0", [hex: :req, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c59db3d838b595b95954a3d0a13782e56881cecfe7ba7b793b1a1a6775273a6e"},
|
||||
@@ -85,8 +91,10 @@
|
||||
"telemetry_metrics_prometheus": {:hex, :telemetry_metrics_prometheus, "1.1.0", "1cc23e932c1ef9aa3b91db257ead31ea58d53229d407e059b29bb962c1505a13", [:mix], [{:plug_cowboy, "~> 2.1", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:telemetry_metrics_prometheus_core, "~> 1.0", [hex: :telemetry_metrics_prometheus_core, repo: "hexpm", optional: false]}], "hexpm", "d43b3659b3244da44fe0275b717701542365d4519b79d9ce895b9719c1ce4d26"},
|
||||
"telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.2.1", "c9755987d7b959b557084e6990990cb96a50d6482c683fb9622a63837f3cd3d8", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "5e2c599da4983c4f88a33e9571f1458bf98b0cf6ba930f1dc3a6e8cf45d5afb6"},
|
||||
"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"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.1", "a48703a25c170eedadca83b11e88985af08d35f37c6f664d6dcfb106a97782fc", [:rebar3], [], "hexpm", "b3a917854ce3ae233619744ad1e0102e05673136776fb2fa76234f3e03b23642"},
|
||||
"usage_rules": {:hex, :usage_rules, "1.2.6", "a7b3f8d6e5d265701139d5714749c37c54bb82230a4c51ec54a12a1e4769b9d1", [: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", "608411b9876a16a9d62a427dbaf42faf458e4cd0a508b3bd7e5ee71502073582"},
|
||||
"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"},
|
||||
"websockex": {:hex, :websockex, "0.5.1", "9de28d37bbe34f371eb46e29b79c94c94fff79f93c960d842fbf447253558eb4", [:mix], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8ef39576ed56bc3804c9cd8626f8b5d6b5721848d2726c0ccd4f05385a3c9f14"},
|
||||
|
||||
Regular → Executable
+53
-1
@@ -7,6 +7,7 @@ Usage:
|
||||
plugin validate
|
||||
plugin test [mix test args...]
|
||||
plugin precommit [mix precommit args...]
|
||||
plugin smoke
|
||||
plugin shell
|
||||
EOF
|
||||
}
|
||||
@@ -22,6 +23,52 @@ host_root="${TRIBES_HOST_ROOT:-$plugin_root/../tribes}"
|
||||
host_root="$(cd "$host_root" && pwd -P)"
|
||||
host_script="$host_root/scripts/plugin"
|
||||
|
||||
json_field() {
|
||||
local field="$1"
|
||||
|
||||
FIELD="$field" mix run --no-start -e '
|
||||
field = System.fetch_env!("FIELD")
|
||||
manifest = "manifest.json" |> File.read!() |> JSON.decode!()
|
||||
value = Map.fetch!(manifest, field)
|
||||
IO.write(value)
|
||||
'
|
||||
}
|
||||
|
||||
run_smoke() {
|
||||
cd "$plugin_root"
|
||||
|
||||
mix compile
|
||||
|
||||
local otp_app
|
||||
local entry_module
|
||||
otp_app="$(json_field otp_app)"
|
||||
entry_module="$(json_field entry_module)"
|
||||
|
||||
local beam_path="_build/dev/lib/$otp_app/ebin/Elixir.$entry_module.beam"
|
||||
[[ -f "$beam_path" ]] || fail "expected entry module beam at $beam_path"
|
||||
|
||||
cd "$host_root"
|
||||
PLUGIN_ROOT="$plugin_root" ENTRY_MODULE="$entry_module" mix run --no-start -e '
|
||||
plugin_root = System.fetch_env!("PLUGIN_ROOT")
|
||||
entry_module = System.fetch_env!("ENTRY_MODULE")
|
||||
|
||||
plugin_root
|
||||
|> Path.join("_build/dev/lib/*/ebin")
|
||||
|> Path.wildcard()
|
||||
|> Enum.each(&(:code.add_patha(String.to_charlist(&1))))
|
||||
|
||||
module = String.to_atom("Elixir." <> entry_module)
|
||||
|
||||
case Code.ensure_loaded(module) do
|
||||
{:module, ^module} ->
|
||||
IO.puts("Loaded #{entry_module}")
|
||||
|
||||
other ->
|
||||
raise "failed to load #{entry_module}: #{inspect(other)}"
|
||||
end
|
||||
'
|
||||
}
|
||||
|
||||
command_name="${1:-}"
|
||||
if [[ -z "$command_name" ]]; then
|
||||
usage
|
||||
@@ -30,7 +77,7 @@ fi
|
||||
shift
|
||||
|
||||
case "$command_name" in
|
||||
validate | test | precommit | shell) ;;
|
||||
validate | test | precommit | smoke | shell) ;;
|
||||
-h | --help | help)
|
||||
usage
|
||||
exit 0
|
||||
@@ -43,6 +90,11 @@ esac
|
||||
|
||||
[[ -x "$host_script" || -f "$host_script" ]] || fail "expected host plugin script at $host_script"
|
||||
|
||||
if [[ "$command_name" == "smoke" ]]; then
|
||||
run_smoke "$@"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "${DEVENV_ROOT:-}" == "$plugin_root" ]]; then
|
||||
command -v devenv >/dev/null 2>&1 || fail "devenv is required when running from the plugin devenv shell"
|
||||
cd "$host_root"
|
||||
|
||||
Reference in New Issue
Block a user