You've already forked tribes-plugin-aether
forked from tribes/tribes-plugin-template
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab297d508d | |||
| 0ba524bad9 | |||
| 36c40e3e25 | |||
| 2e7d201866 | |||
| c338e12170 | |||
| 493a2e0a65 | |||
| 664ed5222f | |||
| fddd616772 | |||
| 569f7e9785 | |||
| 22fd27da14 | |||
| f6b767fbec | |||
| 41d35103dd | |||
| ed0d4f9c0d | |||
| 54d9bdd99c |
+179
@@ -0,0 +1,179 @@
|
|||||||
|
# This file contains the configuration for Credo and you are probably reading
|
||||||
|
# this after creating it with `mix credo.gen.config`.
|
||||||
|
#
|
||||||
|
# If you find anything wrong or unclear in this file, please report an
|
||||||
|
# issue on GitHub: https://github.com/rrrene/credo/issues
|
||||||
|
#
|
||||||
|
%{
|
||||||
|
#
|
||||||
|
# You can have as many configs as you like in the `configs:` field.
|
||||||
|
configs: [
|
||||||
|
%{
|
||||||
|
#
|
||||||
|
# Run any config using `mix credo -C <name>`. If no config name is given
|
||||||
|
# "default" is used.
|
||||||
|
#
|
||||||
|
name: "default",
|
||||||
|
#
|
||||||
|
# These are the files included in the analysis:
|
||||||
|
files: %{
|
||||||
|
#
|
||||||
|
# You can give explicit globs or simply directories.
|
||||||
|
# In the latter case `**/*.{ex,exs}` will be used.
|
||||||
|
#
|
||||||
|
included: [
|
||||||
|
"lib/",
|
||||||
|
"src/",
|
||||||
|
"test/"
|
||||||
|
],
|
||||||
|
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
|
||||||
|
},
|
||||||
|
#
|
||||||
|
# Load and configure plugins here:
|
||||||
|
#
|
||||||
|
plugins: [],
|
||||||
|
#
|
||||||
|
# If you create your own checks, you must specify the source files for
|
||||||
|
# them here, so they can be loaded by Credo before running the analysis.
|
||||||
|
#
|
||||||
|
requires: [],
|
||||||
|
#
|
||||||
|
# If you want to enforce a style guide and need a more traditional linting
|
||||||
|
# experience, you can change `strict` to `true` below:
|
||||||
|
#
|
||||||
|
strict: false,
|
||||||
|
#
|
||||||
|
# To modify the timeout for parsing files, change this value:
|
||||||
|
#
|
||||||
|
parse_timeout: 5000,
|
||||||
|
#
|
||||||
|
# If you want to use uncolored output by default, you can change `color`
|
||||||
|
# to `false` below:
|
||||||
|
#
|
||||||
|
color: true,
|
||||||
|
#
|
||||||
|
# You can customize the parameters of any check by adding a second element
|
||||||
|
# to the tuple.
|
||||||
|
#
|
||||||
|
# To disable a check put `false` as second element:
|
||||||
|
#
|
||||||
|
# {Credo.Check.Design.DuplicatedCode, false}
|
||||||
|
#
|
||||||
|
checks: [
|
||||||
|
#
|
||||||
|
## Consistency Checks
|
||||||
|
#
|
||||||
|
{Credo.Check.Consistency.ExceptionNames, []},
|
||||||
|
{Credo.Check.Consistency.LineEndings, []},
|
||||||
|
{Credo.Check.Consistency.ParameterPatternMatching, []},
|
||||||
|
{Credo.Check.Consistency.SpaceAroundOperators, []},
|
||||||
|
{Credo.Check.Consistency.SpaceInParentheses, []},
|
||||||
|
{Credo.Check.Consistency.TabsOrSpaces, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
## Design Checks
|
||||||
|
#
|
||||||
|
# You can customize the priority of any check
|
||||||
|
# Priority values are: `low, normal, high, higher`
|
||||||
|
#
|
||||||
|
{Credo.Check.Design.AliasUsage, false},
|
||||||
|
# You can also customize the exit_status of each check.
|
||||||
|
# If you don't want TODO comments to cause `mix credo` to fail, just
|
||||||
|
# set this value to 0 (zero).
|
||||||
|
#
|
||||||
|
{Credo.Check.Design.TagTODO, [exit_status: 2]},
|
||||||
|
{Credo.Check.Design.TagFIXME, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
## Readability Checks
|
||||||
|
#
|
||||||
|
{Credo.Check.Readability.AliasOrder, []},
|
||||||
|
{Credo.Check.Readability.FunctionNames, []},
|
||||||
|
{Credo.Check.Readability.LargeNumbers, []},
|
||||||
|
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
|
||||||
|
{Credo.Check.Readability.ModuleAttributeNames, []},
|
||||||
|
{Credo.Check.Readability.ModuleDoc, []},
|
||||||
|
{Credo.Check.Readability.ModuleNames, []},
|
||||||
|
{Credo.Check.Readability.ParenthesesInCondition, []},
|
||||||
|
{Credo.Check.Readability.ParenthesesOnZeroArityDefs, []},
|
||||||
|
{Credo.Check.Readability.PredicateFunctionNames, []},
|
||||||
|
{Credo.Check.Readability.PreferImplicitTry, []},
|
||||||
|
{Credo.Check.Readability.RedundantBlankLines, []},
|
||||||
|
{Credo.Check.Readability.Semicolons, []},
|
||||||
|
{Credo.Check.Readability.SpaceAfterCommas, []},
|
||||||
|
{Credo.Check.Readability.StringSigils, []},
|
||||||
|
{Credo.Check.Readability.TrailingBlankLine, []},
|
||||||
|
{Credo.Check.Readability.TrailingWhiteSpace, []},
|
||||||
|
{Credo.Check.Readability.UnnecessaryAliasExpansion, []},
|
||||||
|
{Credo.Check.Readability.VariableNames, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
## Refactoring Opportunities
|
||||||
|
#
|
||||||
|
{Credo.Check.Refactor.CondStatements, []},
|
||||||
|
{Credo.Check.Refactor.CyclomaticComplexity, false},
|
||||||
|
{Credo.Check.Refactor.FunctionArity, [max_arity: 13]},
|
||||||
|
{Credo.Check.Refactor.LongQuoteBlocks, false},
|
||||||
|
{Credo.Check.Refactor.MapInto, false},
|
||||||
|
{Credo.Check.Refactor.MatchInCondition, []},
|
||||||
|
{Credo.Check.Refactor.NegatedConditionsInUnless, []},
|
||||||
|
{Credo.Check.Refactor.NegatedConditionsWithElse, []},
|
||||||
|
{Credo.Check.Refactor.Nesting, [max_nesting: 7]},
|
||||||
|
{Credo.Check.Refactor.UnlessWithElse, []},
|
||||||
|
{Credo.Check.Refactor.WithClauses, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
## Warnings
|
||||||
|
#
|
||||||
|
{Credo.Check.Warning.BoolOperationOnSameValues, []},
|
||||||
|
{Credo.Check.Warning.ExpensiveEmptyEnumCheck, []},
|
||||||
|
{Credo.Check.Warning.IExPry, []},
|
||||||
|
{Credo.Check.Warning.IoInspect, []},
|
||||||
|
{Credo.Check.Warning.LazyLogging, false},
|
||||||
|
{Credo.Check.Warning.MixEnv, false},
|
||||||
|
{Credo.Check.Warning.OperationOnSameValues, []},
|
||||||
|
{Credo.Check.Warning.OperationWithConstantResult, []},
|
||||||
|
{Credo.Check.Warning.RaiseInsideRescue, []},
|
||||||
|
{Credo.Check.Warning.UnusedEnumOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedFileOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedKeywordOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedListOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedPathOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedRegexOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedStringOperation, []},
|
||||||
|
{Credo.Check.Warning.UnusedTupleOperation, []},
|
||||||
|
{Credo.Check.Warning.UnsafeExec, []},
|
||||||
|
|
||||||
|
#
|
||||||
|
# Checks scheduled for next check update (opt-in for now, just replace `false` with `[]`)
|
||||||
|
|
||||||
|
#
|
||||||
|
# Controversial and experimental checks (opt-in, just replace `false` with `[]`)
|
||||||
|
#
|
||||||
|
{Credo.Check.Readability.StrictModuleLayout, false},
|
||||||
|
{Credo.Check.Consistency.MultiAliasImportRequireUse, false},
|
||||||
|
{Credo.Check.Consistency.UnusedVariableNames, false},
|
||||||
|
{Credo.Check.Design.DuplicatedCode, false},
|
||||||
|
{Credo.Check.Readability.AliasAs, false},
|
||||||
|
{Credo.Check.Readability.MultiAlias, false},
|
||||||
|
{Credo.Check.Readability.Specs, false},
|
||||||
|
{Credo.Check.Readability.SinglePipe, false},
|
||||||
|
{Credo.Check.Readability.WithCustomTaggedTuple, false},
|
||||||
|
{Credo.Check.Refactor.ABCSize, false},
|
||||||
|
{Credo.Check.Refactor.AppendSingleItem, false},
|
||||||
|
{Credo.Check.Refactor.DoubleBooleanNegation, false},
|
||||||
|
{Credo.Check.Refactor.ModuleDependencies, false},
|
||||||
|
{Credo.Check.Refactor.NegatedIsNil, false},
|
||||||
|
{Credo.Check.Refactor.PipeChainStart, false},
|
||||||
|
{Credo.Check.Refactor.VariableRebinding, false},
|
||||||
|
{Credo.Check.Warning.LeakyEnvironment, false},
|
||||||
|
{Credo.Check.Warning.MapGetUnsafePass, false},
|
||||||
|
{Credo.Check.Warning.UnsafeToAtom, false}
|
||||||
|
|
||||||
|
#
|
||||||
|
# Custom checks can be created using `mix credo.gen.check`.
|
||||||
|
#
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
export DIRENV_WARN_TIMEOUT=20s
|
||||||
|
|
||||||
|
eval "$(devenv direnvrc)"
|
||||||
|
|
||||||
|
# `use devenv` supports the same options as the `devenv shell` command.
|
||||||
|
#
|
||||||
|
# To silence all output, use `--quiet`.
|
||||||
|
#
|
||||||
|
# Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true
|
||||||
|
use devenv
|
||||||
+17
@@ -2,11 +2,28 @@
|
|||||||
/deps/
|
/deps/
|
||||||
/_build/
|
/_build/
|
||||||
/dist/
|
/dist/
|
||||||
|
/result
|
||||||
|
/result-*
|
||||||
|
|
||||||
|
# Devenv
|
||||||
|
.devenv*
|
||||||
|
devenv.local.nix
|
||||||
|
|
||||||
|
# Pre-commit
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
/node_modules/
|
/node_modules/
|
||||||
/assets/node_modules/
|
/assets/node_modules/
|
||||||
|
|
||||||
|
# Built plugin assets
|
||||||
|
/priv/static/*.css
|
||||||
|
/priv/static/*.js
|
||||||
|
/priv/static/vendor/
|
||||||
|
|
||||||
|
# Browser tooling
|
||||||
|
.playwright-mcp/
|
||||||
|
|
||||||
# Generated on crash
|
# Generated on crash
|
||||||
erl_crash.dump
|
erl_crash.dump
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,665 @@
|
|||||||
|
# Tribes Plugin Template Agent Guide
|
||||||
|
|
||||||
|
This repository is a template for Tribes plugins. Preserve template placeholders
|
||||||
|
unless a task explicitly asks to rename the template.
|
||||||
|
|
||||||
|
## 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 installing or building Node assets from outside the devenv shell. Do not run raw `npm install` directly.
|
||||||
|
- Do not use raw `mix test` for host-backed plugin suites unless you are
|
||||||
|
deliberately recreating the `plugin test` host environment by hand; use
|
||||||
|
`mix raw_test` only for that low-level debugging path.
|
||||||
|
- Run `scripts/plugin smoke` after changing `mix.exs`, `manifest.json`, entry modules, or host-facing dependency setup.
|
||||||
|
- Keep commits semantic, for example `feat: add template smoke checks`, and include a body except for minimal patches.
|
||||||
|
|
||||||
|
## Plugin Contract
|
||||||
|
|
||||||
|
- `manifest.json` `entry_module` must point at `Tribes.Plugins.MyPlugin.Plugin` until the template is renamed.
|
||||||
|
- Keep the host bridge module at `lib/tribes/plugins/my_plugin/plugin.ex`; after renaming it should become `lib/tribes/plugins/<plugin>/plugin.ex`.
|
||||||
|
- The dev `:tribes` dependency is compile-only so host plugin loading can build `_build/dev` beams without starting a nested host app.
|
||||||
|
- The test `:tribes` dependency is runtime-enabled so host-backed tests can start `Tribes.Repo` and related services.
|
||||||
|
- If the plugin adds cluster-synced Ash resources, use `AshNostrSync` deliberately and document replication semantics in `docs/plugin-contract.md`.
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
- Default pages should render inside host chrome with `Tribes.Plugin.Layouts.app`; keep `ui@1` in `manifest.json` `requires` for host chrome or direct `Tribes.UI` usage.
|
||||||
|
- Prefer TypeScript in `assets/ts` for browser code. The default build emits browser-ready files to `priv/static`.
|
||||||
|
- Keep CSS selectors plugin-scoped, normally prefixed with `.my-plugin`, to avoid collisions with the host or other plugins.
|
||||||
|
- Use the package scripts in `assets/package.json`; do not bypass them with ad-hoc asset commands.
|
||||||
|
|
||||||
|
<!-- 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
|
||||||
|
`social@1`, not semver. Framework APIs are part of the `host_api` foundation, not separate manifest capabilities.
|
||||||
|
- 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 -->
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
# Tribes Plugin Template
|
# Tribes Plugin Template
|
||||||
|
|
||||||
|
**DEPRECATED!** Use [the generator (`tribes-plugin-new`)](https://git.teralink.net/tribes/tribes-plugin-new) instead.
|
||||||
|
|
||||||
Template for creating [Tribes](https://github.com/your-org/tribes) plugins.
|
Template for creating [Tribes](https://github.com/your-org/tribes) plugins.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
@@ -15,11 +17,22 @@ cd your-plugin
|
|||||||
|
|
||||||
3. Edit `manifest.json` — set description, capabilities, requirements
|
3. Edit `manifest.json` — set description, capabilities, requirements
|
||||||
4. Implement your plugin in `lib/your_plugin/plugin.ex`
|
4. Implement your plugin in `lib/your_plugin/plugin.ex`
|
||||||
5. Run tests:
|
5. Run validation, smoke checks, and tests:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mix deps.get
|
mix deps.get
|
||||||
mix test
|
mix tribes.plugin.validate
|
||||||
|
scripts/plugin smoke
|
||||||
|
scripts/plugin test
|
||||||
|
```
|
||||||
|
|
||||||
|
If you are using the shared `tribes` devenv, run through the local helper instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
devenv shell -- plugin validate
|
||||||
|
devenv shell -- plugin smoke
|
||||||
|
devenv shell -- plugin test
|
||||||
|
devenv shell -- plugin precommit
|
||||||
```
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
@@ -27,15 +40,46 @@ mix test
|
|||||||
For local development alongside a Tribes checkout:
|
For local development alongside a Tribes checkout:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Symlink into the host plugins directory
|
# Symlink into the host plugins directory once
|
||||||
cd /path/to/tribes
|
cd /path/to/tribes
|
||||||
ln -s /path/to/your-plugin plugins/your_plugin
|
ln -s /path/to/your-plugin plugins/your_plugin
|
||||||
|
|
||||||
# Start Tribes dev server — your plugin loads automatically
|
# Start Tribes dev server
|
||||||
iex --sname dev -S mix phx.server
|
iex --sname dev -S mix phx.server
|
||||||
```
|
```
|
||||||
|
|
||||||
Edit your plugin source. Phoenix code reloader picks up changes.
|
Then edit the plugin in its own repo. In development, the host now watches
|
||||||
|
symlinked external plugins and automatically:
|
||||||
|
|
||||||
|
- runs `mix compile` for Elixir/HEEx or manifest changes
|
||||||
|
- runs `npm run build --prefix assets` when `assets/package.json` is present
|
||||||
|
- reloads the plugin in the running Tribes VM
|
||||||
|
- triggers a Phoenix browser reload after the rebuild finishes
|
||||||
|
|
||||||
|
That means the normal edit/compile loop is:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/your-plugin
|
||||||
|
mix deps.get
|
||||||
|
|
||||||
|
# in another terminal
|
||||||
|
cd /path/to/tribes
|
||||||
|
iex --sname dev -S mix phx.server
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the plugin repo's devenv shell, the `plugin` helper forwards to the host devenv automatically:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
plugin validate
|
||||||
|
plugin smoke
|
||||||
|
plugin test
|
||||||
|
plugin precommit
|
||||||
|
```
|
||||||
|
|
||||||
|
If your plugin does not need a custom frontend pipeline, you can skip
|
||||||
|
`assets/package.json` and write browser-ready files directly under `assets/js`
|
||||||
|
and `assets/css`; the host dev watcher will copy them into `priv/static` for
|
||||||
|
you in development. The default template uses TypeScript under `assets/ts`.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -43,16 +87,24 @@ Edit your plugin source. Phoenix code reloader picks up changes.
|
|||||||
your_plugin/
|
your_plugin/
|
||||||
├── manifest.json # Plugin metadata (Nix build + runtime)
|
├── manifest.json # Plugin metadata (Nix build + runtime)
|
||||||
├── mix.exs # Dependencies
|
├── mix.exs # Dependencies
|
||||||
|
├── config/ # Host-backed test config
|
||||||
├── lib/
|
├── lib/
|
||||||
│ ├── your_plugin/
|
│ ├── your_plugin/
|
||||||
│ │ ├── plugin.ex # Tribes.Plugin entry point
|
│ │ ├── plugin.ex # Tribes.Plugin entry point
|
||||||
│ │ └── application.ex # OTP supervision tree (optional)
|
│ │ └── application.ex # OTP supervision tree (optional)
|
||||||
│ └── your_plugin_web/
|
│ └── your_plugin_web/
|
||||||
│ └── live/ # LiveView pages
|
│ └── live/ # LiveView pages
|
||||||
├── assets/ # JS/CSS (one bundle per plugin)
|
├── assets/ # TS/CSS (one bundle per plugin)
|
||||||
|
│ ├── ts/ # TypeScript browser entry points
|
||||||
|
│ ├── package.json # Optional build script used by dev + Guix packaging
|
||||||
|
│ ├── tsconfig.json # TypeScript compiler settings
|
||||||
|
│ └── package-lock.json # Optional, recommended for reproducible builds
|
||||||
├── priv/
|
├── priv/
|
||||||
│ ├── static/ # Built assets for release
|
│ ├── static/ # Built assets for release
|
||||||
│ └── repo/migrations/ # Ecto migrations
|
│ └── repo/migrations/ # Ecto migrations
|
||||||
|
├── scripts/
|
||||||
|
│ ├── plugin # Shared devenv/non-devenv test wrapper
|
||||||
|
│ └── rename.sh
|
||||||
└── test/
|
└── test/
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -63,32 +115,85 @@ your_plugin/
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "your_plugin",
|
"name": "your_plugin",
|
||||||
|
"entry_module": "Tribes.Plugins.YourPlugin.Plugin",
|
||||||
|
"host_api": "1",
|
||||||
|
"otp_app": "your_plugin",
|
||||||
"provides": ["some_capability@1"],
|
"provides": ["some_capability@1"],
|
||||||
"requires": ["ecto@1"],
|
"requires": ["ui@1"],
|
||||||
"enhances_with": ["inference@1"]
|
"enhances_with": ["inference@1"]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- **entry_module** — must be `Tribes.Plugins.*.Plugin`
|
||||||
|
- **otp_app** — required and must match `name`
|
||||||
- **provides** — capabilities this plugin makes available
|
- **provides** — capabilities this plugin makes available
|
||||||
- **requires** — hard dependencies (build fails without them)
|
- **requires** — hard plugin/API contracts beyond the `host_api` foundation
|
||||||
|
(build fails without them)
|
||||||
- **enhances_with** — optional dependencies (plugin degrades gracefully)
|
- **enhances_with** — optional dependencies (plugin degrades gracefully)
|
||||||
|
|
||||||
|
The default template includes `ui@1` because the generated LiveView renders
|
||||||
|
inside the host chrome through `Tribes.Plugin.Layouts.app`. Keep `ui@1` when a
|
||||||
|
plugin uses host chrome, imports `Tribes.UI.Components`, or uses `use Tribes.UI`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"requires": ["ui@1"]
|
||||||
|
```
|
||||||
|
|
||||||
|
`host_api` is the versioned foundation contract. It provides the supported
|
||||||
|
Phoenix, Ash, PubSub, data, and cluster-event APIs for plugins. Do not add
|
||||||
|
separate framework capability requirements for APIs that belong to that
|
||||||
|
foundation.
|
||||||
|
|
||||||
See the [Plugin System docs](https://github.com/your-org/tribes/blob/master/docs/PLUGINS.md) for the full specification.
|
See the [Plugin System docs](https://github.com/your-org/tribes/blob/master/docs/PLUGINS.md) for the full specification.
|
||||||
|
|
||||||
|
The default template assumes the plugin owns a top-level page such as
|
||||||
|
`/your_plugin` and any views beneath it. Render normal plugin pages with
|
||||||
|
`Tribes.Plugin.Layouts.app` so they live inside the Tribes chrome/navigation;
|
||||||
|
only omit it for intentionally unwrapped fullscreen surfaces.
|
||||||
|
|
||||||
|
## Plugin Data
|
||||||
|
|
||||||
|
If your plugin adds Ash resources that should replicate cluster-wide, use
|
||||||
|
`extensions: [AshNostrSync]` on those resources and follow the host defaults:
|
||||||
|
|
||||||
|
- persisted attributes sync by default unless explicitly excluded
|
||||||
|
- synced resources should use one UUID primary key by default
|
||||||
|
- `AshNostrSync` requires exactly one primary key; composite keys are not supported
|
||||||
|
- synced resources get extension-managed `deleted_at` soft-delete support by default
|
||||||
|
- destroy actions can use `soft_delete()` when they should tombstone rows
|
||||||
|
- `soft_delete?(false)` is the opt-out if a synced resource should not use the default tombstone model
|
||||||
|
|
||||||
|
Keep resource-specific side effects explicit in actions. `AshNostrSync` owns the
|
||||||
|
default tombstone/filter/projection behavior, but your plugin still decides which
|
||||||
|
actions are semantic deletes and what else they should do.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
Three test levels:
|
The template starts with two host-backed test layers:
|
||||||
|
|
||||||
- **Unit tests** (`test/your_plugin/`) — plugin logic in isolation
|
- **Contract tests** (`test/your_plugin/plugin_contract_test.exs`) — manifest and runtime spec stay aligned
|
||||||
- **Manifest tests** (`test/your_plugin/manifest_test.exs`) — manifest schema validation
|
- **Page tests** (`test/your_plugin/home_page_test.exs`) — the plugin renders through the real host page pipeline
|
||||||
- **Contract tests** (`test/contract_test.exs`) — runtime spec matches manifest
|
|
||||||
|
|
||||||
Run all: `mix test`
|
Run them with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mix tribes.plugin.validate
|
||||||
|
scripts/plugin smoke
|
||||||
|
scripts/plugin test
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `plugin test` / `plugin precommit` for host-backed tests. The helper invokes
|
||||||
|
Mix with the host database/services and host plugin-manager paths, including
|
||||||
|
built-in providers such as `tribes_ui`. Raw `mix test` in a plugin checkout is
|
||||||
|
guarded and prints this guidance; `mix raw_test` / `mix raw_precommit` are
|
||||||
|
available only for unusual manual debugging when you have set the same host
|
||||||
|
environment yourself.
|
||||||
|
|
||||||
## Building for Release
|
## Building for Release
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
MIX_ENV=prod mix compile
|
MIX_ENV=prod mix compile
|
||||||
|
devenv shell -- npm run build --prefix assets
|
||||||
|
|
||||||
mkdir -p dist/your_plugin
|
mkdir -p dist/your_plugin
|
||||||
cp -r _build/prod/lib/your_plugin/ebin dist/your_plugin/
|
cp -r _build/prod/lib/your_plugin/ebin dist/your_plugin/
|
||||||
@@ -96,7 +201,8 @@ cp -r priv dist/your_plugin/
|
|||||||
cp manifest.json dist/your_plugin/
|
cp manifest.json dist/your_plugin/
|
||||||
```
|
```
|
||||||
|
|
||||||
For Nix-based deployment, add your plugin to the host's `plugins.json`.
|
For Guix-based deployment, package your plugin in the `guix-tribes` channel and
|
||||||
|
enable it from the node config.
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
// Plugin JavaScript entry point.
|
|
||||||
//
|
|
||||||
// This file is served by the host at /plugins-assets/my_plugin/my_plugin.js
|
|
||||||
// and included in the page layout if declared in manifest.json assets.global_js.
|
|
||||||
//
|
|
||||||
// To register LiveView hooks:
|
|
||||||
//
|
|
||||||
// window.TribesPluginHooks = window.TribesPluginHooks || {};
|
|
||||||
// window.TribesPluginHooks["MyPluginHook"] = {
|
|
||||||
// mounted() {
|
|
||||||
// console.log("MyPlugin hook mounted");
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
|
|
||||||
console.log("my_plugin loaded");
|
|
||||||
Generated
+29
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "my-plugin-assets",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "my-plugin-assets",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"name": "my-plugin-assets",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm run check && mkdir -p ../priv/static && cp -r css/. ../priv/static && tsc --project tsconfig.json",
|
||||||
|
"check": "tsc --project tsconfig.json --noEmit"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^6.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Plugin TypeScript entry point.
|
||||||
|
//
|
||||||
|
// This file is compiled to /priv/static/my_plugin.js, served by the host at
|
||||||
|
// /plugins-assets/my_plugin/my_plugin.js, and included when declared in
|
||||||
|
// manifest.json assets.global_js.
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
TribesPluginHooks?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TribesPluginHooks = window.TribesPluginHooks ?? {};
|
||||||
|
|
||||||
|
console.info("my_plugin loaded");
|
||||||
|
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"exactOptionalPropertyTypes": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"outDir": "../priv/static",
|
||||||
|
"rootDir": "ts"
|
||||||
|
},
|
||||||
|
"include": ["ts/**/*.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import Config
|
||||||
|
|
||||||
|
import_config "#{config_env()}.exs"
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import Config
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import Config
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import Config
|
||||||
|
|
||||||
|
import_config "../../tribes/config/config.exs"
|
||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"devenv": {
|
||||||
|
"locked": {
|
||||||
|
"dir": "src/modules",
|
||||||
|
"lastModified": 1777109791,
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv",
|
||||||
|
"rev": "025b6ba9903b96a55ac21a9a63fa290a6da5afe6",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"dir": "src/modules",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1767039857,
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"git-hooks": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"gitignore": "gitignore",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1776796298,
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"rev": "3cfd774b0a530725a077e17354fbdb87ea1c4aad",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"git-hooks",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1762808025,
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs-src": "nixpkgs-src"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1776852779,
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "devenv-nixpkgs",
|
||||||
|
"rev": "ec3063523dcd911aeadb50faa589f237cdab5853",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"ref": "rolling",
|
||||||
|
"repo": "devenv-nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs-src": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1776329215,
|
||||||
|
"narHash": "sha256-a8BYi3mzoJ/AcJP8UldOx8emoPRLeWqALZWu4ZvjPXw=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "b86751bc4085f48661017fa226dee99fab6c651b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"devenv": "devenv",
|
||||||
|
"git-hooks": "git-hooks",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"pre-commit-hooks": [
|
||||||
|
"git-hooks"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
env = {
|
||||||
|
MIX_OS_DEPS_COMPILE_PARTITION_COUNT = 8;
|
||||||
|
NODE_ENV = "development";
|
||||||
|
};
|
||||||
|
|
||||||
|
packages = with pkgs;
|
||||||
|
[
|
||||||
|
git
|
||||||
|
alejandra
|
||||||
|
prettier
|
||||||
|
]
|
||||||
|
++ lib.optionals pkgs.stdenv.isLinux [
|
||||||
|
inotify-tools
|
||||||
|
];
|
||||||
|
|
||||||
|
languages = {
|
||||||
|
elixir = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.elixir_1_19;
|
||||||
|
};
|
||||||
|
|
||||||
|
javascript = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.nodejs_24;
|
||||||
|
npm.enable = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
dotenv.enable = true;
|
||||||
|
devenv.warnOnNewVersion = false;
|
||||||
|
|
||||||
|
git-hooks.hooks = {
|
||||||
|
alejandra.enable = true;
|
||||||
|
prettier.enable = true;
|
||||||
|
prettier.files = "\\.(js|ts|tsx|css)$";
|
||||||
|
check-added-large-files = {
|
||||||
|
enable = true;
|
||||||
|
args = ["--maxkb=131072"];
|
||||||
|
};
|
||||||
|
mix-format.enable = true;
|
||||||
|
mix-format.files = "\\.(ex|exs|heex)$";
|
||||||
|
};
|
||||||
|
|
||||||
|
enterShell = ''
|
||||||
|
echo
|
||||||
|
elixir --version
|
||||||
|
echo -n "Node.js "
|
||||||
|
node --version
|
||||||
|
echo
|
||||||
|
'';
|
||||||
|
|
||||||
|
scripts = {
|
||||||
|
plugin.exec = ''bash "$DEVENV_ROOT/scripts/plugin" "$@"'';
|
||||||
|
};
|
||||||
|
}
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
|
||||||
|
inputs:
|
||||||
|
nixpkgs:
|
||||||
|
url: github:cachix/devenv-nixpkgs/rolling
|
||||||
|
|
||||||
|
# If you're using non-OSS software, you can set allowUnfree to true.
|
||||||
|
# allowUnfree: true
|
||||||
|
|
||||||
|
# If you have more than one devenv you can merge them.
|
||||||
|
# imports:
|
||||||
|
# - ./backend
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Checklist
|
||||||
|
|
||||||
|
## After Renaming
|
||||||
|
|
||||||
|
- Run `./scripts/rename.sh your_plugin YourPlugin`.
|
||||||
|
- Confirm `manifest.json` `name`, `otp_app`, and `entry_module`.
|
||||||
|
- Confirm the bridge module path is `lib/tribes/plugins/your_plugin/plugin.ex`.
|
||||||
|
- Rename asset files and update manifest asset names if needed.
|
||||||
|
- Run `mix deps.get`.
|
||||||
|
- Run `scripts/plugin smoke`.
|
||||||
|
- Run `scripts/plugin test`.
|
||||||
|
|
||||||
|
## Before Committing
|
||||||
|
|
||||||
|
- Run `mix format`.
|
||||||
|
- Run `scripts/plugin precommit`.
|
||||||
|
- If assets changed, run `devenv shell -- npm run build --prefix assets`.
|
||||||
|
- Check `git status --short` for generated files that should not be committed.
|
||||||
|
- Commit with a semantic subject and a useful body unless the patch is minimal.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Development
|
||||||
|
|
||||||
|
## Local Setup
|
||||||
|
|
||||||
|
This template expects a sibling Tribes checkout at `../tribes`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mix deps.get
|
||||||
|
devenv shell -- plugin test
|
||||||
|
```
|
||||||
|
|
||||||
|
To load the plugin in the host during development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ../tribes
|
||||||
|
ln -s ../tribes-plugin-template plugins/my_plugin
|
||||||
|
iex --sname dev -S mix phx.server
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `TRIBES_HOST_ROOT=/path/to/tribes` if your host checkout is not a sibling.
|
||||||
|
|
||||||
|
## Command Wrapper
|
||||||
|
|
||||||
|
Use `scripts/plugin` for host-aware workflows:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scripts/plugin validate
|
||||||
|
scripts/plugin test
|
||||||
|
scripts/plugin precommit
|
||||||
|
scripts/plugin smoke
|
||||||
|
```
|
||||||
|
|
||||||
|
Inside the template devenv shell, the `plugin` command is available and forwards
|
||||||
|
through the same wrapper.
|
||||||
|
|
||||||
|
Prefer `plugin test` over raw `mix test` for host-backed plugin suites. The
|
||||||
|
wrapper runs Mix with the host database/services and points the host plugin
|
||||||
|
manager at the host manifest plus built-in providers such as `tribes_ui`. Use
|
||||||
|
`mix raw_test` / `mix raw_precommit` only for deliberate low-level debugging.
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
Browser code lives in `assets/ts` and is compiled to `priv/static`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
devenv shell -- npm run build --prefix assets
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the wrapper form for installs too:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
devenv shell -- npm install --prefix assets
|
||||||
|
```
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Plugin Contract
|
||||||
|
|
||||||
|
## Manifest
|
||||||
|
|
||||||
|
`manifest.json` is the runtime contract consumed by Tribes:
|
||||||
|
|
||||||
|
- `name` and `otp_app` must match the Mix application name.
|
||||||
|
- `entry_module` must be a loadable `Tribes.Plugins.*.Plugin` module.
|
||||||
|
- `provides` declares capabilities exported by the plugin.
|
||||||
|
- `requires` declares hard plugin/API contracts beyond the `host_api` foundation.
|
||||||
|
- `enhances_with` declares optional host capabilities.
|
||||||
|
- `migrations` should be `true` when `priv/repo/migrations` contains plugin migrations.
|
||||||
|
- `children` should be `true` when the plugin starts its own supervision tree.
|
||||||
|
|
||||||
|
Declare `ui@1` in `requires` when rendering through `Tribes.Plugin.Layouts.app`,
|
||||||
|
importing `Tribes.UI.Components`, or using `use Tribes.UI`. The facade is
|
||||||
|
intentionally provider-backed, so host chrome and UI consumers must make the
|
||||||
|
runtime dependency explicit.
|
||||||
|
|
||||||
|
`host_api` is the versioned foundation contract. It provides the supported
|
||||||
|
Phoenix, Ash, PubSub, data, and cluster-event APIs for plugins. Do not add
|
||||||
|
separate framework capability requirements for APIs that belong to that
|
||||||
|
foundation.
|
||||||
|
|
||||||
|
Default LiveView pages should wrap their content in `Tribes.Plugin.Layouts.app`
|
||||||
|
and use `on_mount({Tribes.Plugin.LiveUserAuth, :live_user_optional})`. This
|
||||||
|
keeps plugin pages inside the replaceable Tribes chrome while avoiding a hard
|
||||||
|
compile-time dependency on host web macros.
|
||||||
|
|
||||||
|
## Entry Modules
|
||||||
|
|
||||||
|
The template uses a thin host bridge:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
defmodule Tribes.Plugins.MyPlugin.Plugin do
|
||||||
|
defdelegate register(context), to: MyPlugin.Plugin
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep plugin implementation code in `MyPlugin.Plugin`. Keep the bridge stable so
|
||||||
|
the host plugin manager can load it from the plugin build output.
|
||||||
|
|
||||||
|
## Host Dependencies
|
||||||
|
|
||||||
|
`tribes_plugin_api` is the release-facing `host_api` foundation. It carries the
|
||||||
|
supported plugin behaviours, helpers, Phoenix/Ash data surface, and sync DSL.
|
||||||
|
Do not add the full `:tribes` app as a production dependency.
|
||||||
|
|
||||||
|
The template intentionally splits the `:tribes` path dependency by environment:
|
||||||
|
|
||||||
|
- In `:dev`, `:tribes` is compile-only. This lets `mix compile` create the
|
||||||
|
entry-module beam expected by the host plugin manager without starting a
|
||||||
|
nested Tribes application.
|
||||||
|
- In `:test`, `:tribes` is runtime-enabled. Host-backed tests need the real
|
||||||
|
repository, endpoint pipeline, and plugin contract helpers.
|
||||||
|
|
||||||
|
## Ash Resources
|
||||||
|
|
||||||
|
For cluster-synced plugin data, add Ash resources under the plugin namespace and
|
||||||
|
use `extensions: [AshNostrSync]` only for data that should replicate across the
|
||||||
|
cluster. Prefer one UUID primary key per synced resource. Document any local-only
|
||||||
|
tables, retention policies, or side effects in plugin docs.
|
||||||
+8
-56
@@ -1,34 +1,18 @@
|
|||||||
defmodule MyPlugin.Plugin do
|
defmodule MyPlugin.Plugin do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Tribes plugin entry point.
|
Tribes plugin entry point.
|
||||||
|
|
||||||
Implements the `Tribes.Plugin` behaviour. This module is referenced by
|
|
||||||
`entry_module` in manifest.json and is called by the host's PluginManager
|
|
||||||
during startup.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Once Tribes.Plugin.Base is available, replace the manual implementation
|
use Tribes.Plugin.Base, otp_app: :my_plugin
|
||||||
# with:
|
|
||||||
#
|
|
||||||
# use Tribes.Plugin.Base, otp_app: :my_plugin
|
|
||||||
#
|
|
||||||
# and override only register/1.
|
|
||||||
|
|
||||||
# @behaviour Tribes.Plugin
|
@impl true
|
||||||
|
def register(context) do
|
||||||
def register(_context) do
|
super(context)
|
||||||
manifest = read_manifest()
|
|> Map.merge(%{
|
||||||
|
|
||||||
%{
|
|
||||||
name: manifest["name"],
|
|
||||||
version: manifest["version"],
|
|
||||||
provides: manifest["provides"] || [],
|
|
||||||
requires: manifest["requires"] || [],
|
|
||||||
enhances_with: manifest["enhances_with"] || [],
|
|
||||||
nav_items: [
|
nav_items: [
|
||||||
%{
|
%{
|
||||||
label: "My Plugin",
|
label: "My Plugin",
|
||||||
path: "/plugins/my_plugin",
|
path: "/my_plugin",
|
||||||
icon: nil,
|
icon: nil,
|
||||||
requires: [],
|
requires: [],
|
||||||
order: 50
|
order: 50
|
||||||
@@ -36,46 +20,14 @@ defmodule MyPlugin.Plugin do
|
|||||||
],
|
],
|
||||||
pages: [
|
pages: [
|
||||||
%{
|
%{
|
||||||
path: "/plugins/my_plugin",
|
path: "/my_plugin",
|
||||||
live_view: MyPluginWeb.HomeLive,
|
live_view: MyPluginWeb.HomeLive,
|
||||||
layout: nil
|
layout: nil
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
api_routes: [],
|
api_routes: [],
|
||||||
plugs: [],
|
plugs: [],
|
||||||
children: [],
|
|
||||||
global_js: get_in(manifest, ["assets", "global_js"]) || [],
|
|
||||||
global_css: get_in(manifest, ["assets", "global_css"]) || [],
|
|
||||||
migrations_path: migrations_path(manifest),
|
|
||||||
hooks: %{}
|
hooks: %{}
|
||||||
}
|
})
|
||||||
end
|
|
||||||
|
|
||||||
defp read_manifest do
|
|
||||||
manifest_path()
|
|
||||||
|> File.read!()
|
|
||||||
|> Jason.decode!()
|
|
||||||
end
|
|
||||||
|
|
||||||
defp manifest_path do
|
|
||||||
# In a release, manifest.json sits alongside ebin/ in the plugin directory.
|
|
||||||
# In dev mode (path dep), it's at the project root.
|
|
||||||
case :code.priv_dir(:my_plugin) do
|
|
||||||
{:error, :bad_name} ->
|
|
||||||
# Dev mode fallback: relative to project root
|
|
||||||
Path.join(__DIR__, "../../../manifest.json") |> Path.expand()
|
|
||||||
|
|
||||||
priv_dir ->
|
|
||||||
priv_dir |> to_string() |> Path.join("../manifest.json") |> Path.expand()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp migrations_path(manifest) do
|
|
||||||
if manifest["migrations"] do
|
|
||||||
case :code.priv_dir(:my_plugin) do
|
|
||||||
{:error, :bad_name} -> nil
|
|
||||||
priv_dir -> priv_dir |> to_string() |> Path.join("repo/migrations")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ defmodule MyPluginWeb.HomeLive do
|
|||||||
@moduledoc """
|
@moduledoc """
|
||||||
Example LiveView page for the plugin.
|
Example LiveView page for the plugin.
|
||||||
|
|
||||||
This page is registered in the plugin spec and mounted by the host
|
This page is registered in the plugin spec, mounted by the host at
|
||||||
at /plugins/my_plugin.
|
/my_plugin, and rendered inside the Tribes chrome by default.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# In dev mode (plugin loaded as path dep), you can use host macros:
|
# In dev mode (plugin loaded as path dep), you can use host macros:
|
||||||
@@ -12,19 +12,25 @@ defmodule MyPluginWeb.HomeLive do
|
|||||||
# For release builds (standalone OTP app), use Phoenix.LiveView directly:
|
# For release builds (standalone OTP app), use Phoenix.LiveView directly:
|
||||||
use Phoenix.LiveView
|
use Phoenix.LiveView
|
||||||
|
|
||||||
|
on_mount({Tribes.Plugin.LiveUserAuth, :live_user_optional})
|
||||||
|
|
||||||
|
alias Tribes.Plugin.Layouts
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
{:ok, assign(socket, :page_title, "My Plugin")}
|
{:ok, assign(socket, :page_title, "My Plugin")}
|
||||||
end
|
end
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="p-6">
|
<Layouts.app flash={@flash} current_scope={@current_scope}>
|
||||||
|
<div class="my-plugin p-6">
|
||||||
<h1 class="text-2xl font-bold">My Plugin</h1>
|
<h1 class="text-2xl font-bold">My Plugin</h1>
|
||||||
<p class="mt-2 text-base-content/70">
|
<p class="mt-2 text-base-content/70">
|
||||||
This is a Tribes plugin. Edit this page in
|
This is a Tribes plugin. Edit this page in
|
||||||
<code>lib/my_plugin_web/live/home_live.ex</code>.
|
<code>lib/my_plugin_web/live/home_live.ex</code>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</Layouts.app>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
defmodule Tribes.Plugins.MyPlugin.Plugin do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
defdelegate register(context), to: MyPlugin.Plugin
|
||||||
|
end
|
||||||
+3
-2
@@ -2,10 +2,11 @@
|
|||||||
"name": "my_plugin",
|
"name": "my_plugin",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "TODO: Describe what this plugin does",
|
"description": "TODO: Describe what this plugin does",
|
||||||
"entry_module": "MyPlugin.Plugin",
|
"entry_module": "Tribes.Plugins.MyPlugin.Plugin",
|
||||||
"host_api": "1",
|
"host_api": "1",
|
||||||
|
"otp_app": "my_plugin",
|
||||||
"provides": [],
|
"provides": [],
|
||||||
"requires": ["ecto@1"],
|
"requires": ["ui@1"],
|
||||||
"enhances_with": [],
|
"enhances_with": [],
|
||||||
"assets": {
|
"assets": {
|
||||||
"global_js": ["my_plugin.js"],
|
"global_js": ["my_plugin.js"],
|
||||||
|
|||||||
@@ -9,7 +9,14 @@ defmodule MyPlugin.MixProject do
|
|||||||
elixirc_paths: elixirc_paths(Mix.env()),
|
elixirc_paths: elixirc_paths(Mix.env()),
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
deps: deps(),
|
deps: deps(),
|
||||||
aliases: aliases()
|
aliases: aliases(),
|
||||||
|
usage_rules: usage_rules()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
def cli do
|
||||||
|
[
|
||||||
|
preferred_envs: [precommit: :test, raw_precommit: :test, raw_test: :test]
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -26,21 +33,80 @@ defmodule MyPlugin.MixProject do
|
|||||||
|
|
||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
# Host dependency — provides Tribes.Plugin behaviour, types, and test helpers.
|
# Plugin API dependency for local development alongside a tribes checkout.
|
||||||
#
|
#
|
||||||
# For local development alongside a tribes checkout:
|
# For CI or standalone development, this can be replaced with a published
|
||||||
{:tribes, path: "../tribes", only: [:dev, :test]},
|
# package once tribes_plugin_api is released.
|
||||||
#
|
{:tribes_plugin_api, path: "../tribes/tribes_plugin_api", runtime: false},
|
||||||
# For CI or standalone development (when not co-located with tribes):
|
{:tribes_plugin, path: "../tribes-plugin-new", only: [:dev, :test], runtime: false},
|
||||||
# {:tribes, github: "your-org/tribes", branch: "master", only: [:dev, :test]},
|
{:igniter, "~> 0.7", only: [:dev, :test], runtime: false},
|
||||||
|
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
|
||||||
|
{:lazy_html, ">= 0.1.0", only: :test},
|
||||||
|
{:phoenix, "~> 1.8"},
|
||||||
|
{:phoenix_html, "~> 4.1"},
|
||||||
|
{:phoenix_live_view, "~> 1.1.0"},
|
||||||
|
{:usage_rules, "~> 1.2", only: :dev}
|
||||||
|
] ++ tribes_deps(Mix.env())
|
||||||
|
end
|
||||||
|
|
||||||
{:jason, "~> 1.2"}
|
defp tribes_deps(:dev), do: [{:tribes, path: "../tribes", only: :dev, runtime: false}]
|
||||||
|
defp tribes_deps(:test), do: [{:tribes, path: "../tribes", only: :test}]
|
||||||
|
defp tribes_deps(_), do: []
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
defp aliases do
|
defp aliases do
|
||||||
[
|
[
|
||||||
test: ["test"]
|
test: &plugin_test_message/1,
|
||||||
|
raw_test: &raw_test/1,
|
||||||
|
lint: ["format --check-formatted", "credo"],
|
||||||
|
precommit: &plugin_precommit_message/1,
|
||||||
|
raw_precommit: &raw_precommit/1
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp plugin_test_message(_args) do
|
||||||
|
Mix.raise("""
|
||||||
|
This plugin test suite is host-backed. Use `plugin test` or `scripts/plugin test`.
|
||||||
|
|
||||||
|
For low-level debugging only, set up the host environment yourself and run `mix raw_test`.
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp plugin_precommit_message(_args) do
|
||||||
|
Mix.raise("""
|
||||||
|
This plugin precommit is host-backed. Use `plugin precommit` or `scripts/plugin precommit`.
|
||||||
|
|
||||||
|
For low-level debugging only, set up the host environment yourself and run `mix raw_precommit`.
|
||||||
|
""")
|
||||||
|
end
|
||||||
|
|
||||||
|
defp raw_precommit(args) do
|
||||||
|
Mix.Task.run("format")
|
||||||
|
Mix.Task.run("compile", ["--warnings-as-errors"])
|
||||||
|
Mix.Task.run("credo", ["--strict", "--all"])
|
||||||
|
Mix.Task.run("deps.unlock", ["--unused"])
|
||||||
|
raw_test(args)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp raw_test(args), do: Mix.Tasks.Test.run(args)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
%{
|
||||||
|
"absinthe": {:hex, :absinthe, "1.10.0", "58e4923c2a96bf12cba9aad9298f36f624533039f2e5305badf0d60904eee0a0", [:mix], [{:dataloader, "~> 1.0.0 or ~> 2.0", [hex: :dataloader, repo: "hexpm", optional: true]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}, {:opentelemetry_process_propagator, "~> 0.2.1 or ~> 0.3", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6810d21f13a2b4783d1b7f468c63a5174d769a3035f188e3fc93f440128a46af"},
|
||||||
|
"absinthe_phoenix": {:hex, :absinthe_phoenix, "2.0.4", "f36999412fbd6a2339abb5b7e24a4cc9492bbc7909d5806deeef83b06f55c508", [:mix], [{:absinthe, "~> 1.5", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_plug, "~> 1.5", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.5", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.13 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}], "hexpm", "66617ee63b725256ca16264364148b10b19e2ecb177488cd6353584f2e6c1cf3"},
|
||||||
|
"absinthe_plug": {:hex, :absinthe_plug, "1.5.9", "4f66fd46aecf969b349dd94853e6132db6d832ae6a4b951312b6926ad4ee7ca3", [:mix], [{:absinthe, "~> 1.7", [hex: :absinthe, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "dcdc84334b0e9e2cd439bd2653678a822623f212c71088edf0a4a7d03f1fa225"},
|
||||||
|
"argon2_elixir": {:hex, :argon2_elixir, "4.1.3", "4f28318286f89453364d7fbb53e03d4563fd7ed2438a60237eba5e426e97785f", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "7c295b8d8e0eaf6f43641698f962526cdf87c6feb7d14bd21e599271b510608c"},
|
||||||
|
"ash": {:hex, :ash, "3.24.3", "f7280a43c5e64f769a450f3dd59ace6dcd73edcdd0de7599815b1b31f59292fb", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c1022f8c549632137cbc8956f07bb4981405297f5abe7a752b4dffac175c3381"},
|
||||||
|
"ash_authentication": {:hex, :ash_authentication, "4.13.7", "421b5ddb516026f6794435980a632109ec116af2afa68a45e15fb48b41c92cfa", [:mix], [{:argon2_elixir, "~> 4.0", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:ash, "~> 3.7", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_postgres, ">= 2.6.8 and < 3.0.0-0", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:assent, "> 0.2.0 and < 0.3.0", [hex: :assent, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:finch, "~> 0.19", [hex: :finch, repo: "hexpm", optional: false]}, {:igniter, "~> 0.4", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.5", [hex: :joken, repo: "hexpm", optional: false]}, {:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, "~> 2.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.2", [hex: :splode, repo: "hexpm", optional: false]}], "hexpm", "0d45ac3fdcca6902dabbe161ce63e9cea8f90583863c2e14261c9309e5837121"},
|
||||||
|
"ash_authentication_phoenix": {:hex, :ash_authentication_phoenix, "2.16.0", "02045ecde9eeb30ab1bfffbdf693c64426af24902bcd533765eba725b9b9f46f", [:mix], [{:ash, "~> 3.0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.10", [hex: :ash_authentication, repo: "hexpm", optional: false]}, {:ash_phoenix, ">= 2.3.11 and < 3.0.0-0", [hex: :ash_phoenix, repo: "hexpm", optional: false]}, {:bcrypt_elixir, "~> 3.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: false]}, {:gettext, "~> 0.26 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:igniter, ">= 0.5.25 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_html_helpers, "~> 1.0", [hex: :phoenix_html_helpers, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 1.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:slugify, "~> 1.3", [hex: :slugify, repo: "hexpm", optional: false]}], "hexpm", "1107a45af771ee7c02ebe82abcaf9a778096e66b3e6cb2b6e614d22d1fe385f7"},
|
||||||
|
"ash_graphql": {:hex, :ash_graphql, "1.9.4", "3e75eaa0917e1085c938aadfec8205cb028a161842d13b9304475f56a8b1c6a0", [:mix], [{:absinthe, "~> 1.7", [hex: :absinthe, repo: "hexpm", optional: false]}, {:absinthe_phoenix, "~> 2.0", [hex: :absinthe_phoenix, repo: "hexpm", optional: true]}, {:absinthe_plug, "~> 1.4", [hex: :absinthe_plug, repo: "hexpm", optional: false]}, {:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, ">= 0.5.28 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.10", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "da3a7b3026ca31b006bf675f6dcbe49c4575c42cad794684373fc7a51893165b"},
|
||||||
|
"ash_phoenix": {:hex, :ash_phoenix, "2.3.21", "757e227e4d835b748a31932305e307e2db3bd3229c219ddad0277f6598d3e392", [:mix], [{:ash, ">= 3.5.13 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, "~> 0.6", [hex: :igniter, repo: "hexpm", optional: true]}, {:inertia, "~> 2.3", [hex: :inertia, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.6 or ~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.20.3 or ~> 1.0-rc.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.29 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "4e28b92709e86856927c7c32effa9336f0aa3daec769883993bf42ba49cce90f"},
|
||||||
|
"ash_postgres": {:hex, :ash_postgres, "2.9.0", "5867a8f91f136add9b11fb44c22f7636538ed29ff8601da80470cfe792b81cf2", [:mix], [{:ash, "~> 3.24", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, "~> 0.6", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.13", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:spark, ">= 2.3.4 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "81f284b7550b9b9cd19bf2a68abc222066a8e6e3c4e88143c9c39ede37df6b42"},
|
||||||
|
"ash_sql": {:hex, :ash_sql, "0.6.1", "d2788d020a5fe7b1052cdf529f97937979634e912baaea957b750b1f8f38ba52", [:mix], [{:ash, "~> 3.24", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, ">= 3.13.4 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "16efb0a5551269faafb32f173c144fbf85cce1576206005f6dbf244b6b403969"},
|
||||||
|
"assent": {:hex, :assent, "0.2.13", "11226365d2d8661d23e9a2cf94d3255e81054ff9d88ac877f28bfdf38fa4ef31", [:mix], [{:certifi, ">= 0.0.0", [hex: :certifi, repo: "hexpm", optional: true]}, {:finch, "~> 0.15", [hex: :finch, repo: "hexpm", optional: true]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: true]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:req, "~> 0.4", [hex: :req, repo: "hexpm", optional: true]}, {:ssl_verify_fun, ">= 0.0.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: true]}], "hexpm", "bf9f351b01dd6bceea1d1f157f05438f6765ce606e6eb8d29296003d29bf6eab"},
|
||||||
|
"bandit": {:hex, :bandit, "1.10.4", "02b9734c67c5916a008e7eb7e2ba68aaea6f8177094a5f8d95f1fb99069aac17", [:mix], [{:hpax, "~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.18", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "a5faf501042ac1f31d736d9d4a813b3db4ef812e634583b6a457b0928798a51d"},
|
||||||
|
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.3.2", "d50091e3c9492d73e17fc1e1619a9b09d6a5ef99160eb4d736926fd475a16ca3", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "471be5151874ae7931911057d1467d908955f93554f7a6cd1b7d804cac8cef53"},
|
||||||
|
"bech32": {:hex, :bech32, "1.0.0", "85a3bb58c408d735b5becb39e8a23536660ec0df1ef0afee72377c130939de1b", [:mix], [], "hexpm", "f781b8524c30a524922613d97c1858c27bd9f639b4e6b350f4a4843ee97607d3"},
|
||||||
|
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
|
||||||
|
"castore": {:hex, :castore, "1.0.18", "5e43ef0ec7d31195dfa5a65a86e6131db999d074179d2ba5a8de11fe14570f55", [:mix], [], "hexpm", "f393e4fe6317829b158fb74d86eb681f737d2fe326aa61ccf6293c4104957e34"},
|
||||||
|
"cc_precompiler": {:hex, :cc_precompiler, "0.1.11", "8c844d0b9fb98a3edea067f94f616b3f6b29b959b6b3bf25fee94ffe34364768", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3427232caf0835f94680e5bcf082408a70b48ad68a5f5c0b02a3bea9f3a075b9"},
|
||||||
|
"cldr_utils": {:hex, :cldr_utils, "2.29.5", "f43161e04acb4016f5841b2320d69120d51827f5346babb2227893a2c5916dc8", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "962d3a2028b232ee0a5373941dc411028a9442f53444a4d5d2c354f687db1835"},
|
||||||
|
"comeonin": {:hex, :comeonin, "5.5.1", "5113e5f3800799787de08a6e0db307133850e635d34e9fab23c70b6501669510", [:mix], [], "hexpm", "65aac8f19938145377cee73973f192c5645873dcf550a8a6b18187d17c13ccdb"},
|
||||||
|
"cowboy": {:hex, :cowboy, "2.14.2", "4008be1df6ade45e4f2a4e9e2d22b36d0b5aba4e20b0a0d7049e28d124e34847", [:make, :rebar3], [{:cowlib, ">= 2.16.0 and < 3.0.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, ">= 1.8.0 and < 3.0.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "569081da046e7b41b5df36aa359be71a0c8874e5b9cff6f747073fc57baf1ab9"},
|
||||||
|
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
|
||||||
|
"cowlib": {:hex, :cowlib, "2.16.0", "54592074ebbbb92ee4746c8a8846e5605052f29309d3a873468d76cdf932076f", [:make, :rebar3], [], "hexpm", "7f478d80d66b747344f0ea7708c187645cfcc08b11aa424632f78e25bf05db51"},
|
||||||
|
"credo": {:hex, :credo, "1.7.18", "5c5596bf7aedf9c8c227f13272ac499fe8eae6237bd326f2f07dfc173786f042", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a189d164685fd945809e862fe76a7420c4398fa288d76257662aecb909d6b3e5"},
|
||||||
|
"crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"},
|
||||||
|
"db_connection": {:hex, :db_connection, "2.10.0", "8ff756471e41765bd5563b633f73e9a94bbc138816e8644bb17d0d91bf260a95", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02cdd01b45efb1b550e68edbbea41be32de9b24bb07e1ea0e9cbc522ac377e54"},
|
||||||
|
"decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"},
|
||||||
|
"digital_token": {:hex, :digital_token, "1.0.0", "454a4444061943f7349a51ef74b7fb1ebd19e6a94f43ef711f7dae88c09347df", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "8ed6f5a8c2fa7b07147b9963db506a1b4c7475d9afca6492136535b064c9e9e6"},
|
||||||
|
"dns_cluster": {:hex, :dns_cluster, "0.2.0", "aa8eb46e3bd0326bd67b84790c561733b25c5ba2fe3c7e36f28e88f384ebcb33", [:mix], [], "hexpm", "ba6f1893411c69c01b9e8e8f772062535a4cf70f3f35bcc964a324078d8c8240"},
|
||||||
|
"ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"},
|
||||||
|
"ecto_sql": {:hex, :ecto_sql, "3.13.5", "2f8282b2ad97bf0f0d3217ea0a6fff320ead9e2f8770f810141189d182dc304e", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.13.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aa36751f4e6a2b56ae79efb0e088042e010ff4935fc8684e74c23b1f49e25fdc"},
|
||||||
|
"elixir_make": {:hex, :elixir_make, "0.9.0", "6484b3cd8c0cee58f09f05ecaf1a140a8c97670671a6a0e7ab4dc326c3109726", [:mix], [], "hexpm", "db23d4fd8b757462ad02f8aa73431a426fe6671c80b200d9710caf3d1dd0ffdb"},
|
||||||
|
"esbuild": {:hex, :esbuild, "0.10.0", "b0aa3388a1c23e727c5a3e7427c932d89ee791746b0081bbe56103e9ef3d291f", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "468489cda427b974a7cc9f03ace55368a83e1a7be12fba7e30969af78e5f8c70"},
|
||||||
|
"ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"},
|
||||||
|
"ex_cldr": {:hex, :ex_cldr, "2.47.2", "c866f4b45523abd25eea3e5252eb91364296dd15bddf970db1c78cd38f25df9a", [:mix], [{:cldr_utils, "~> 2.29", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19 or ~> 1.0", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "4a7cef380a1c2546166b45d6ee5e8e2f707ea695b12ae6dadd250201588b4f16"},
|
||||||
|
"ex_cldr_calendars": {:hex, :ex_cldr_calendars, "2.4.2", "31d5d7a65add622dc4bf16fc1bfe5ef378d71cfdaf02fb33c7d0742c585f6117", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: true]}, {:ex_cldr_numbers, "~> 2.36", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.20", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:ex_doc, "~> 0.21", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "ab69fd04bc1ae18baf9d2e57335d4754c5ac263076ea397eb112621702251fe5"},
|
||||||
|
"ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.17.1", "89947c7102ff1b46fc46095624239a1c3d72499b19ed650597630771d9e4a662", [:mix], [{:ex_cldr, "~> 2.38", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "e266a0a61f4c7d83608154d49b59e4d7485b2aaa7ba1d0e17b3c55910595de51"},
|
||||||
|
"ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.25.6", "6db974ab2b430b5733994c2bfbe98a69e25eeb076b876a929791ff521f8fdd96", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 2.4", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.20", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:tz, "~> 0.26", [hex: :tz, repo: "hexpm", optional: true]}], "hexpm", "926ff5662b849f86088832ee66b61a96aab0fa5a54d5e14240e08ad3030663e2"},
|
||||||
|
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.38.1", "e5124e288a8e672831e10d39530ecb5329bc9af2169709ebfbadc814cae7d4fb", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.45", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.17", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "4f95738f1dc4e821485e52226666f7691c9276bf6eba49cba8d23c8a2db05e84"},
|
||||||
|
"expo": {:hex, :expo, "1.1.1", "4202e1d2ca6e2b3b63e02f69cfe0a404f77702b041d02b58597c00992b601db5", [:mix], [], "hexpm", "5fb308b9cb359ae200b7e23d37c76978673aa1b06e2b3075d814ce12c5811640"},
|
||||||
|
"file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"},
|
||||||
|
"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"},
|
||||||
|
"jose": {:hex, :jose, "1.11.12", "06e62b467b61d3726cbc19e9b5489f7549c37993de846dfb3ee8259f9ed208b3", [:mix, :rebar3], [], "hexpm", "31e92b653e9210b696765cdd885437457de1add2a9011d92f8cf63e4641bab7b"},
|
||||||
|
"lazy_html": {:hex, :lazy_html, "0.1.11", "136c8e9cd616b4f4e9c1562daa683880891120b759606dc4c3b6b18058ba5d79", [: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", "3b1be592929c31eca1a21673d25696e5c14cddfe922d9d1a3e3b48be4163883b"},
|
||||||
|
"lib_secp256k1": {:hex, :lib_secp256k1, "0.7.1", "53cad778b8da3a29e453a7a477517d99fb5f13f615c8050eb2db8fd1dce7a1db", [:make, :mix], [{:elixir_make, "~> 0.9", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "78bdd3661a17448aff5aeec5ca74c8ddbc09b01f0ecfa3ba1aba3e8ae47ab2b3"},
|
||||||
|
"libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"},
|
||||||
|
"mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"},
|
||||||
|
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||||
|
"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"},
|
||||||
|
"phoenix_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"},
|
||||||
|
"phoenix_html_helpers": {:hex, :phoenix_html_helpers, "1.0.1", "7eed85c52eff80a179391036931791ee5d2f713d76a81d0d2c6ebafe1e11e5ec", [:mix], [{:phoenix_html, "~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "cffd2385d1fa4f78b04432df69ab8da63dc5cf63e07b713a4dcf36a3740e3090"},
|
||||||
|
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.8.7", "405880012cb4b706f26dd1c6349125bfc903fb9e44d1ea668adaf4e04d4884b7", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:ecto_sqlite3_extras, "~> 1.1.7 or ~> 1.2.0", [hex: :ecto_sqlite3_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "3a8625cab39ec261d48a13b7468dc619c0ede099601b084e343968309bd4d7d7"},
|
||||||
|
"phoenix_live_view": {:hex, :phoenix_live_view, "1.1.28", "8a8e123d018025f756605a2fb02a4854f0d3cd7b207f710fef1fd5d9d72d0254", [:mix], [{:igniter, ">= 0.6.16 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:lazy_html, "~> 0.1.0", [hex: :lazy_html, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0 or ~> 1.8.0-rc", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.15", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "24faad535b65089642c3a7d84088109dc58f49c1f1c5a978659855d643466353"},
|
||||||
|
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.2.0", "ff3a5616e1bed6804de7773b92cbccfc0b0f473faf1f63d7daf1206c7aeaaa6f", [:mix], [], "hexpm", "adc313a5bf7136039f63cfd9668fde73bba0765e0614cba80c06ac9460ff3e96"},
|
||||||
|
"phoenix_template": {:hex, :phoenix_template, "1.0.4", "e2092c132f3b5e5b2d49c96695342eb36d0ed514c5b252a77048d5969330d639", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "2c0c81f0e5c6753faf5cca2f229c9709919aba34fab866d3bc05060c9c444206"},
|
||||||
|
"phoenix_view": {:hex, :phoenix_view, "2.0.4", "b45c9d9cf15b3a1af5fb555c674b525391b6a1fe975f040fb4d913397b31abf4", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0 or ~> 4.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "4e992022ce14f31fe57335db27a28154afcc94e9983266835bb3040243eb620b"},
|
||||||
|
"picosat_elixir": {:hex, :picosat_elixir, "0.2.3", "bf326d0f179fbb3b706bb2c15fbc367dacfa2517157d090fdfc32edae004c597", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f76c9db2dec9d2561ffaa9be35f65403d53e984e8cd99c832383b7ab78c16c66"},
|
||||||
|
"plug": {:hex, :plug, "1.19.1", "09bac17ae7a001a68ae393658aa23c7e38782be5c5c00c80be82901262c394c0", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "560a0017a8f6d5d30146916862aaf9300b7280063651dd7e532b8be168511e62"},
|
||||||
|
"plug_cowboy": {:hex, :plug_cowboy, "2.8.1", "5aa391a5e8d1ac3192e36a3bcaff12b5fd6ef6c7e29b53a38e63a860783e77d0", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "4c200288673d5bc86a0ab7dc6a2a069176a74e5d573ef62740a1c517458a5f26"},
|
||||||
|
"plug_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"},
|
||||||
|
"postgrex": {:hex, :postgrex, "0.22.0", "fb027b58b6eab1f6de5396a2abcdaaeb168f9ed4eccbb594e6ac393b02078cbd", [:mix], [{:db_connection, "~> 2.9", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "a68c4261e299597909e03e6f8ff5a13876f5caadaddd0d23af0d0a61afcc5d84"},
|
||||||
|
"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"},
|
||||||
|
"telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"},
|
||||||
|
"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_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"},
|
||||||
|
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
|
||||||
|
"yaml_elixir": {:hex, :yaml_elixir, "2.12.1", "d74f2d82294651b58dac849c45a82aaea639766797359baff834b64439f6b3f4", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "d9ac16563c737d55f9bfeed7627489156b91268a3a21cd55c54eb2e335207fed"},
|
||||||
|
"ymlr": {:hex, :ymlr, "5.1.5", "0b9207c7940be3f2bc29b77cd55109d5aa2f4dcde6575942017335769e6f5628", [:mix], [], "hexpm", "7030cb240c46850caeb3b01be745307632be319b15f03083136f6251f49b516d"},
|
||||||
|
}
|
||||||
Executable
+104
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
plugin validate
|
||||||
|
plugin test [mix test args...]
|
||||||
|
plugin precommit [mix precommit args...]
|
||||||
|
plugin smoke
|
||||||
|
plugin shell
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "plugin: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
|
||||||
|
plugin_root="$(cd "$script_dir/.." && pwd -P)"
|
||||||
|
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
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
|
||||||
|
case "$command_name" in
|
||||||
|
validate | test | precommit | smoke | shell) ;;
|
||||||
|
-h | --help | help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
fail "unknown command: $command_name"
|
||||||
|
;;
|
||||||
|
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"
|
||||||
|
exec devenv shell -- bash ./scripts/plugin "$command_name" "$plugin_root" "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec bash "$host_script" "$command_name" "$plugin_root" "$@"
|
||||||
+27
-5
@@ -44,17 +44,39 @@ fi
|
|||||||
if [ -d "test/my_plugin" ]; then
|
if [ -d "test/my_plugin" ]; then
|
||||||
mv "test/my_plugin" "test/$SNAKE"
|
mv "test/my_plugin" "test/$SNAKE"
|
||||||
fi
|
fi
|
||||||
|
if [ -d "lib/tribes/plugins/my_plugin" ]; then
|
||||||
|
mv "lib/tribes/plugins/my_plugin" "lib/tribes/plugins/$SNAKE"
|
||||||
|
fi
|
||||||
|
|
||||||
# Replace in all text files
|
# Replace in all text files (portable across GNU/BSD sed)
|
||||||
find . -type f \( -name '*.ex' -o -name '*.exs' -o -name '*.json' -o -name '*.js' -o -name '*.css' -o -name '*.md' -o -name '*.yml' -o -name '*.yaml' -o -name '.formatter.exs' \) -exec \
|
sed_in_place() {
|
||||||
|
file=$1
|
||||||
|
|
||||||
|
if sed --version >/dev/null 2>&1; then
|
||||||
|
sed -i \
|
||||||
|
-e "s/my_plugin/$SNAKE/g" \
|
||||||
|
-e "s/MyPlugin/$MODULE/g" \
|
||||||
|
-e "s/my-plugin/$SNAKE/g" \
|
||||||
|
"$file"
|
||||||
|
else
|
||||||
sed -i '' \
|
sed -i '' \
|
||||||
-e "s/my_plugin/$SNAKE/g" \
|
-e "s/my_plugin/$SNAKE/g" \
|
||||||
-e "s/MyPlugin/$MODULE/g" \
|
-e "s/MyPlugin/$MODULE/g" \
|
||||||
-e "s/my-plugin/$SNAKE/g" \
|
-e "s/my-plugin/$SNAKE/g" \
|
||||||
{} +
|
"$file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
sed_in_place "$file"
|
||||||
|
done < <(
|
||||||
|
find . \( -name ".devenv" -o -name ".git" -o -name "deps" -o -name "node_modules" \) -prune -o -type f \
|
||||||
|
\( -name '*.ex' -o -name '*.exs' -o -name '*.json' -o -name '*.js' -o -name '*.ts' -o -name '*.css' -o -name '*.md' -o -name '*.yml' -o -name '*.yaml' -o -name '.formatter.exs' \) \
|
||||||
|
-print0
|
||||||
|
)
|
||||||
|
|
||||||
# Rename asset files
|
# Rename asset files
|
||||||
for ext in js css; do
|
for ext in js ts css; do
|
||||||
if [ -f "assets/$ext/my_plugin.$ext" ]; then
|
if [ -f "assets/$ext/my_plugin.$ext" ]; then
|
||||||
mv "assets/$ext/my_plugin.$ext" "assets/$ext/$SNAKE.$ext"
|
mv "assets/$ext/my_plugin.$ext" "assets/$ext/$SNAKE.$ext"
|
||||||
fi
|
fi
|
||||||
@@ -65,4 +87,4 @@ done
|
|||||||
|
|
||||||
echo "Done. Review the changes, then:"
|
echo "Done. Review the changes, then:"
|
||||||
echo " 1. Edit manifest.json — set description, capabilities"
|
echo " 1. Edit manifest.json — set description, capabilities"
|
||||||
echo " 2. Run: mix deps.get && mix test"
|
echo " 2. Run: mix deps.get && scripts/plugin smoke && scripts/plugin test"
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
defmodule MyPlugin.ContractTest do
|
|
||||||
@moduledoc """
|
|
||||||
Contract compliance tests.
|
|
||||||
|
|
||||||
These verify that the plugin conforms to the Tribes plugin contract.
|
|
||||||
When Tribes.Plugin.ContractTest is available (host dep loaded),
|
|
||||||
replace this file with:
|
|
||||||
|
|
||||||
defmodule MyPlugin.ContractTest do
|
|
||||||
use Tribes.Plugin.ContractTest,
|
|
||||||
plugin: MyPlugin.Plugin,
|
|
||||||
otp_app: :my_plugin
|
|
||||||
end
|
|
||||||
|
|
||||||
Until then, this file provides a standalone equivalent.
|
|
||||||
"""
|
|
||||||
|
|
||||||
use ExUnit.Case, async: true
|
|
||||||
|
|
||||||
@plugin MyPlugin.Plugin
|
|
||||||
@manifest_path Path.join(__DIR__, "../manifest.json") |> Path.expand()
|
|
||||||
|
|
||||||
setup_all do
|
|
||||||
manifest = @manifest_path |> File.read!() |> Jason.decode!()
|
|
||||||
spec = @plugin.register(%{pubsub: nil, repo: nil})
|
|
||||||
%{manifest: manifest, spec: spec}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "runtime provides matches manifest provides", %{manifest: manifest, spec: spec} do
|
|
||||||
normalise = fn cap ->
|
|
||||||
if String.contains?(cap, "@"), do: cap, else: "#{cap}@1"
|
|
||||||
end
|
|
||||||
|
|
||||||
manifest_caps = manifest["provides"] |> Enum.map(normalise) |> Enum.sort()
|
|
||||||
runtime_caps = spec.provides |> Enum.map(normalise) |> Enum.sort()
|
|
||||||
|
|
||||||
assert manifest_caps == runtime_caps,
|
|
||||||
"manifest provides #{inspect(manifest_caps)} != runtime provides #{inspect(runtime_caps)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "runtime requires matches manifest requires", %{manifest: manifest, spec: spec} do
|
|
||||||
normalise = fn cap ->
|
|
||||||
if String.contains?(cap, "@"), do: cap, else: "#{cap}@1"
|
|
||||||
end
|
|
||||||
|
|
||||||
manifest_caps = manifest["requires"] |> Enum.map(normalise) |> Enum.sort()
|
|
||||||
runtime_caps = spec.requires |> Enum.map(normalise) |> Enum.sort()
|
|
||||||
|
|
||||||
assert manifest_caps == runtime_caps,
|
|
||||||
"manifest requires #{inspect(manifest_caps)} != runtime requires #{inspect(runtime_caps)}"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "spec has all required keys", %{spec: spec} do
|
|
||||||
required_keys = [
|
|
||||||
:name,
|
|
||||||
:version,
|
|
||||||
:provides,
|
|
||||||
:requires,
|
|
||||||
:nav_items,
|
|
||||||
:pages,
|
|
||||||
:children,
|
|
||||||
:global_js,
|
|
||||||
:global_css,
|
|
||||||
:migrations_path,
|
|
||||||
:hooks
|
|
||||||
]
|
|
||||||
|
|
||||||
for key <- required_keys do
|
|
||||||
assert Map.has_key?(spec, key), "spec must contain #{inspect(key)}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "name in spec matches manifest", %{manifest: manifest, spec: spec} do
|
|
||||||
assert spec.name == manifest["name"]
|
|
||||||
end
|
|
||||||
|
|
||||||
test "migrations_path exists if manifest declares migrations", %{manifest: manifest, spec: spec} do
|
|
||||||
if manifest["migrations"] do
|
|
||||||
assert is_binary(spec.migrations_path),
|
|
||||||
"migrations_path must be set when manifest declares migrations: true"
|
|
||||||
|
|
||||||
assert File.dir?(spec.migrations_path),
|
|
||||||
"migrations_path #{inspect(spec.migrations_path)} must be an existing directory"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
defmodule MyPlugin.HomePageTest do
|
||||||
|
use Tribes.PluginTest.PageCase, plugin: Tribes.Plugins.MyPlugin.Plugin
|
||||||
|
|
||||||
|
test "renders the plugin home page for signed-out visitors", %{conn: conn} do
|
||||||
|
{:ok, view, html} = live(conn, "/my_plugin")
|
||||||
|
|
||||||
|
assert html =~ "My Plugin"
|
||||||
|
assert html =~ "This is a Tribes plugin."
|
||||||
|
assert has_element?(view, "#plugin-nav-my-plugin", "My Plugin")
|
||||||
|
end
|
||||||
|
|
||||||
|
test "dispatches top-level subpaths to the plugin page", %{conn: conn} do
|
||||||
|
{:ok, _view, html} = live(conn, "/my_plugin/example")
|
||||||
|
|
||||||
|
assert html =~ "My Plugin"
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
defmodule MyPlugin.ManifestTest do
|
|
||||||
use ExUnit.Case, async: true
|
|
||||||
|
|
||||||
@manifest_path Path.join(__DIR__, "../../manifest.json") |> Path.expand()
|
|
||||||
|
|
||||||
setup_all do
|
|
||||||
content = File.read!(@manifest_path)
|
|
||||||
manifest = Jason.decode!(content)
|
|
||||||
%{manifest: manifest}
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "manifest.json" do
|
|
||||||
test "is valid JSON", %{manifest: manifest} do
|
|
||||||
assert is_map(manifest)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "has required fields", %{manifest: manifest} do
|
|
||||||
required = ["name", "version", "entry_module", "host_api", "provides", "requires"]
|
|
||||||
|
|
||||||
for field <- required do
|
|
||||||
assert Map.has_key?(manifest, field),
|
|
||||||
"manifest.json must contain #{inspect(field)}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "name matches OTP app name", %{manifest: manifest} do
|
|
||||||
assert manifest["name"] == "my_plugin"
|
|
||||||
end
|
|
||||||
|
|
||||||
test "entry_module is a valid Elixir module name", %{manifest: manifest} do
|
|
||||||
module_name = manifest["entry_module"]
|
|
||||||
assert is_binary(module_name)
|
|
||||||
assert String.starts_with?(module_name, "Elixir.") or not String.contains?(module_name, " ")
|
|
||||||
end
|
|
||||||
|
|
||||||
test "provides contains valid capability identifiers", %{manifest: manifest} do
|
|
||||||
for cap <- manifest["provides"] do
|
|
||||||
assert Regex.match?(~r/^[a-z][a-z0-9_]*(@\d+)?$/, cap),
|
|
||||||
"invalid capability identifier: #{inspect(cap)}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "requires contains valid capability identifiers", %{manifest: manifest} do
|
|
||||||
for cap <- manifest["requires"] do
|
|
||||||
assert Regex.match?(~r/^[a-z][a-z0-9_]*(@\d+)?$/, cap),
|
|
||||||
"invalid capability identifier: #{inspect(cap)}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "host_api is a string version", %{manifest: manifest} do
|
|
||||||
assert is_binary(manifest["host_api"])
|
|
||||||
end
|
|
||||||
|
|
||||||
test "entry_module matches actual plugin module", %{manifest: manifest} do
|
|
||||||
module = String.to_atom("Elixir.#{manifest["entry_module"]}")
|
|
||||||
assert Code.ensure_loaded?(module), "entry_module #{manifest["entry_module"]} must be loadable"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
defmodule MyPlugin.PluginContractTest do
|
||||||
|
use Tribes.PluginTest.ContractTest, plugin: Tribes.Plugins.MyPlugin.Plugin
|
||||||
|
end
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
defmodule MyPlugin.PluginTest do
|
|
||||||
use ExUnit.Case, async: true
|
|
||||||
|
|
||||||
describe "register/1" do
|
|
||||||
setup do
|
|
||||||
context = %{pubsub: nil, repo: nil}
|
|
||||||
%{spec: MyPlugin.Plugin.register(context)}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "returns plugin name and version", %{spec: spec} do
|
|
||||||
assert spec.name == "my_plugin"
|
|
||||||
assert is_binary(spec.version)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "provides is a list of capability strings", %{spec: spec} do
|
|
||||||
assert is_list(spec.provides)
|
|
||||||
|
|
||||||
for cap <- spec.provides do
|
|
||||||
assert is_binary(cap), "capability must be a string, got: #{inspect(cap)}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "requires is a list of capability strings", %{spec: spec} do
|
|
||||||
assert is_list(spec.requires)
|
|
||||||
|
|
||||||
for cap <- spec.requires do
|
|
||||||
assert is_binary(cap), "capability must be a string, got: #{inspect(cap)}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "nav items have required fields", %{spec: spec} do
|
|
||||||
assert is_list(spec.nav_items)
|
|
||||||
|
|
||||||
for item <- spec.nav_items do
|
|
||||||
assert is_binary(item.label), "nav item must have a label"
|
|
||||||
assert is_binary(item.path), "nav item must have a path"
|
|
||||||
assert is_integer(item.order), "nav item must have an integer order"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "pages reference existing modules", %{spec: spec} do
|
|
||||||
assert is_list(spec.pages)
|
|
||||||
|
|
||||||
for page <- spec.pages do
|
|
||||||
assert is_binary(page.path), "page must have a path"
|
|
||||||
assert is_atom(page.live_view), "page must have a live_view module"
|
|
||||||
assert Code.ensure_loaded?(page.live_view), "module #{page.live_view} must be loadable"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
test "children are valid child specs", %{spec: spec} do
|
|
||||||
assert is_list(spec.children)
|
|
||||||
end
|
|
||||||
|
|
||||||
test "asset lists are string lists", %{spec: spec} do
|
|
||||||
assert is_list(spec.global_js)
|
|
||||||
assert is_list(spec.global_css)
|
|
||||||
|
|
||||||
for js <- spec.global_js, do: assert(is_binary(js))
|
|
||||||
for css <- spec.global_css, do: assert(is_binary(css))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Reference in New Issue
Block a user