You've already forked tribes-plugin-aether
forked from tribes/tribes-plugin-template
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 41d35103dd | |||
| ed0d4f9c0d | |||
| 54d9bdd99c |
21
README.md
21
README.md
@@ -27,15 +27,19 @@ mix test
|
|||||||
For local development alongside a Tribes checkout:
|
For local development alongside a Tribes checkout:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Build plugin code once (host loads BEAM from _build/dev/lib/<otp_app>/ebin)
|
||||||
|
cd /path/to/your-plugin
|
||||||
|
mix compile
|
||||||
|
|
||||||
# Symlink into the host plugins directory
|
# Symlink into the host plugins directory
|
||||||
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.
|
When you change plugin Elixir code, re-run `mix compile` in the plugin repo.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -63,12 +67,17 @@ 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": ["ecto@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 dependencies (build fails without them)
|
||||||
- **enhances_with** — optional dependencies (plugin degrades gracefully)
|
- **enhances_with** — optional dependencies (plugin degrades gracefully)
|
||||||
@@ -85,6 +94,14 @@ Three test levels:
|
|||||||
|
|
||||||
Run all: `mix test`
|
Run all: `mix test`
|
||||||
|
|
||||||
|
For DB setup/migrations in local development, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mix tribes.migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs Tribes + Parrhesia + plugin migrations via `Tribes.Release`.
|
||||||
|
|
||||||
## Building for Release
|
## Building for Release
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -58,24 +58,44 @@ defmodule MyPlugin.Plugin do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp manifest_path do
|
defp manifest_path do
|
||||||
# In a release, manifest.json sits alongside ebin/ in the plugin directory.
|
project_manifest = Path.join(__DIR__, "../../manifest.json") |> Path.expand()
|
||||||
# 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 ->
|
candidates =
|
||||||
priv_dir |> to_string() |> Path.join("../manifest.json") |> Path.expand()
|
case :code.priv_dir(:my_plugin) do
|
||||||
end
|
{:error, :bad_name} ->
|
||||||
|
[project_manifest]
|
||||||
|
|
||||||
|
priv_dir ->
|
||||||
|
priv_dir = to_string(priv_dir)
|
||||||
|
|
||||||
|
[
|
||||||
|
Path.join(priv_dir, "../manifest.json") |> Path.expand(),
|
||||||
|
project_manifest
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
first_existing_path(candidates) || project_manifest
|
||||||
end
|
end
|
||||||
|
|
||||||
defp migrations_path(manifest) do
|
defp migrations_path(manifest) do
|
||||||
if manifest["migrations"] do
|
if manifest["migrations"] do
|
||||||
case :code.priv_dir(:my_plugin) do
|
candidates =
|
||||||
{:error, :bad_name} -> nil
|
case :code.priv_dir(:my_plugin) do
|
||||||
priv_dir -> priv_dir |> to_string() |> Path.join("repo/migrations")
|
{:error, :bad_name} ->
|
||||||
end
|
[Path.join(__DIR__, "../../priv/repo/migrations") |> Path.expand()]
|
||||||
|
|
||||||
|
priv_dir ->
|
||||||
|
[
|
||||||
|
Path.join(to_string(priv_dir), "repo/migrations") |> Path.expand(),
|
||||||
|
Path.join(__DIR__, "../../priv/repo/migrations") |> Path.expand()
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
first_existing_path(candidates)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp first_existing_path(paths) do
|
||||||
|
Enum.find(paths, &File.exists?/1)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
5
lib/tribes/plugins/my_plugin/plugin.ex
Normal file
5
lib/tribes/plugins/my_plugin/plugin.ex
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
defmodule Tribes.Plugins.MyPlugin.Plugin do
|
||||||
|
@moduledoc false
|
||||||
|
|
||||||
|
defdelegate register(context), to: MyPlugin.Plugin
|
||||||
|
end
|
||||||
@@ -2,8 +2,9 @@
|
|||||||
"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": ["ecto@1"],
|
||||||
"enhances_with": [],
|
"enhances_with": [],
|
||||||
|
|||||||
2
mix.exs
2
mix.exs
@@ -29,7 +29,7 @@ defmodule MyPlugin.MixProject do
|
|||||||
# Host dependency — provides Tribes.Plugin behaviour, types, and test helpers.
|
# Host dependency — provides Tribes.Plugin behaviour, types, and test helpers.
|
||||||
#
|
#
|
||||||
# For local development alongside a tribes checkout:
|
# For local development alongside a tribes checkout:
|
||||||
{:tribes, path: "../tribes", only: [:dev, :test]},
|
{:tribes, path: "../tribes", only: [:dev, :test], runtime: false},
|
||||||
#
|
#
|
||||||
# For CI or standalone development (when not co-located with tribes):
|
# For CI or standalone development (when not co-located with tribes):
|
||||||
# {:tribes, github: "your-org/tribes", branch: "master", only: [:dev, :test]},
|
# {:tribes, github: "your-org/tribes", branch: "master", only: [:dev, :test]},
|
||||||
|
|||||||
@@ -45,13 +45,32 @@ if [ -d "test/my_plugin" ]; then
|
|||||||
mv "test/my_plugin" "test/$SNAKE"
|
mv "test/my_plugin" "test/$SNAKE"
|
||||||
fi
|
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() {
|
||||||
sed -i '' \
|
file=$1
|
||||||
-e "s/my_plugin/$SNAKE/g" \
|
|
||||||
-e "s/MyPlugin/$MODULE/g" \
|
if sed --version >/dev/null 2>&1; then
|
||||||
-e "s/my-plugin/$SNAKE/g" \
|
sed -i \
|
||||||
{} +
|
-e "s/my_plugin/$SNAKE/g" \
|
||||||
|
-e "s/MyPlugin/$MODULE/g" \
|
||||||
|
-e "s/my-plugin/$SNAKE/g" \
|
||||||
|
"$file"
|
||||||
|
else
|
||||||
|
sed -i '' \
|
||||||
|
-e "s/my_plugin/$SNAKE/g" \
|
||||||
|
-e "s/MyPlugin/$MODULE/g" \
|
||||||
|
-e "s/my-plugin/$SNAKE/g" \
|
||||||
|
"$file"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
while IFS= read -r -d '' file; do
|
||||||
|
sed_in_place "$file"
|
||||||
|
done < <(
|
||||||
|
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' \) \
|
||||||
|
-print0
|
||||||
|
)
|
||||||
|
|
||||||
# Rename asset files
|
# Rename asset files
|
||||||
for ext in js css; do
|
for ext in js css; do
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ defmodule MyPlugin.ContractTest do
|
|||||||
|
|
||||||
defmodule MyPlugin.ContractTest do
|
defmodule MyPlugin.ContractTest do
|
||||||
use Tribes.Plugin.ContractTest,
|
use Tribes.Plugin.ContractTest,
|
||||||
plugin: MyPlugin.Plugin,
|
plugin: Tribes.Plugins.MyPlugin.Plugin,
|
||||||
otp_app: :my_plugin
|
otp_app: :my_plugin
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ defmodule MyPlugin.ContractTest do
|
|||||||
|
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
@plugin MyPlugin.Plugin
|
@plugin Tribes.Plugins.MyPlugin.Plugin
|
||||||
@manifest_path Path.join(__DIR__, "../manifest.json") |> Path.expand()
|
@manifest_path Path.join(__DIR__, "../manifest.json") |> Path.expand()
|
||||||
|
|
||||||
setup_all do
|
setup_all do
|
||||||
|
|||||||
@@ -15,7 +15,15 @@ defmodule MyPlugin.ManifestTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "has required fields", %{manifest: manifest} do
|
test "has required fields", %{manifest: manifest} do
|
||||||
required = ["name", "version", "entry_module", "host_api", "provides", "requires"]
|
required = [
|
||||||
|
"name",
|
||||||
|
"version",
|
||||||
|
"entry_module",
|
||||||
|
"host_api",
|
||||||
|
"otp_app",
|
||||||
|
"provides",
|
||||||
|
"requires"
|
||||||
|
]
|
||||||
|
|
||||||
for field <- required do
|
for field <- required do
|
||||||
assert Map.has_key?(manifest, field),
|
assert Map.has_key?(manifest, field),
|
||||||
@@ -25,12 +33,17 @@ defmodule MyPlugin.ManifestTest do
|
|||||||
|
|
||||||
test "name matches OTP app name", %{manifest: manifest} do
|
test "name matches OTP app name", %{manifest: manifest} do
|
||||||
assert manifest["name"] == "my_plugin"
|
assert manifest["name"] == "my_plugin"
|
||||||
|
assert manifest["otp_app"] == manifest["name"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "entry_module is a valid Elixir module name", %{manifest: manifest} do
|
test "entry_module uses Tribes.Plugins namespace and Plugin suffix", %{manifest: manifest} do
|
||||||
module_name = manifest["entry_module"]
|
module_name = manifest["entry_module"]
|
||||||
assert is_binary(module_name)
|
assert is_binary(module_name)
|
||||||
assert String.starts_with?(module_name, "Elixir.") or not String.contains?(module_name, " ")
|
|
||||||
|
assert Regex.match?(
|
||||||
|
~r/^Tribes\.Plugins\.[A-Z][A-Za-z0-9_]*(\.[A-Z][A-Za-z0-9_]*)*\.Plugin$/,
|
||||||
|
module_name
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "provides contains valid capability identifiers", %{manifest: manifest} do
|
test "provides contains valid capability identifiers", %{manifest: manifest} do
|
||||||
@@ -53,7 +66,9 @@ defmodule MyPlugin.ManifestTest do
|
|||||||
|
|
||||||
test "entry_module matches actual plugin module", %{manifest: manifest} do
|
test "entry_module matches actual plugin module", %{manifest: manifest} do
|
||||||
module = String.to_atom("Elixir.#{manifest["entry_module"]}")
|
module = String.to_atom("Elixir.#{manifest["entry_module"]}")
|
||||||
assert Code.ensure_loaded?(module), "entry_module #{manifest["entry_module"]} must be loadable"
|
|
||||||
|
assert Code.ensure_loaded?(module),
|
||||||
|
"entry_module #{manifest["entry_module"]} must be loadable"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user