From f6b767fbeca867c3ef8a483dd983880881bd373d Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Wed, 8 Apr 2026 13:11:37 +0200 Subject: [PATCH] Update plugin template for host-driven dev reload --- README.md | 37 ++++++++++++--- assets/package-lock.json | 12 +++++ assets/package.json | 8 ++++ lib/my_plugin/plugin.ex | 80 +++----------------------------- mix.exs | 14 +++--- mix.lock | 13 ++++++ test/contract_test.exs | 2 +- test/my_plugin/manifest_test.exs | 2 +- 8 files changed, 79 insertions(+), 89 deletions(-) create mode 100644 assets/package-lock.json create mode 100644 assets/package.json create mode 100644 mix.lock diff --git a/README.md b/README.md index 08bbac3..6942c51 100644 --- a/README.md +++ b/README.md @@ -27,11 +27,7 @@ mix test For local development alongside a Tribes checkout: ```bash -# Build plugin code once (host loads BEAM from _build/dev/lib//ebin) -cd /path/to/your-plugin -mix compile - -# Symlink into the host plugins directory +# Symlink into the host plugins directory once cd /path/to/tribes ln -s /path/to/your-plugin plugins/your_plugin @@ -39,7 +35,30 @@ ln -s /path/to/your-plugin plugins/your_plugin iex --sname dev -S mix phx.server ``` -When you change plugin Elixir code, re-run `mix compile` in the plugin repo. +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 loop is: + +```bash +cd /path/to/your-plugin +mix deps.get +mix test + +# in another terminal +cd /path/to/tribes +iex --sname dev -S mix phx.server +``` + +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. ## Project Structure @@ -54,6 +73,8 @@ your_plugin/ │ └── your_plugin_web/ │ └── live/ # LiveView pages ├── assets/ # JS/CSS (one bundle per plugin) +│ ├── package.json # Optional build script used by dev + Guix packaging +│ └── package-lock.json # Optional, recommended for reproducible builds ├── priv/ │ ├── static/ # Built assets for release │ └── repo/migrations/ # Ecto migrations @@ -106,6 +127,7 @@ This runs Tribes + Parrhesia + plugin migrations via `Tribes.Release`. ```bash MIX_ENV=prod mix compile +npm run build --prefix assets mkdir -p dist/your_plugin cp -r _build/prod/lib/your_plugin/ebin dist/your_plugin/ @@ -113,7 +135,8 @@ cp -r priv 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 diff --git a/assets/package-lock.json b/assets/package-lock.json new file mode 100644 index 0000000..261eb12 --- /dev/null +++ b/assets/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "my-plugin-assets", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "my-plugin-assets", + "version": "0.1.0" + } + } +} diff --git a/assets/package.json b/assets/package.json new file mode 100644 index 0000000..a333a51 --- /dev/null +++ b/assets/package.json @@ -0,0 +1,8 @@ +{ + "name": "my-plugin-assets", + "private": true, + "version": "0.1.0", + "scripts": { + "build": "mkdir -p ../priv/static && cp -r css/. ../priv/static && cp -r js/. ../priv/static" + } +} diff --git a/lib/my_plugin/plugin.ex b/lib/my_plugin/plugin.ex index 6ad579f..16fd2dd 100644 --- a/lib/my_plugin/plugin.ex +++ b/lib/my_plugin/plugin.ex @@ -1,30 +1,14 @@ defmodule MyPlugin.Plugin do @moduledoc """ 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 - # with: - # - # use Tribes.Plugin.Base, otp_app: :my_plugin - # - # and override only register/1. + use Tribes.Plugin.Base, otp_app: :my_plugin - # @behaviour Tribes.Plugin - - def register(_context) do - manifest = read_manifest() - - %{ - name: manifest["name"], - version: manifest["version"], - provides: manifest["provides"] || [], - requires: manifest["requires"] || [], - enhances_with: manifest["enhances_with"] || [], + @impl true + def register(context) do + super(context) + |> Map.merge(%{ nav_items: [ %{ label: "My Plugin", @@ -43,59 +27,7 @@ defmodule MyPlugin.Plugin do ], api_routes: [], plugs: [], - children: [], - global_js: get_in(manifest, ["assets", "global_js"]) || [], - global_css: get_in(manifest, ["assets", "global_css"]) || [], - migrations_path: migrations_path(manifest), hooks: %{} - } - end - - defp read_manifest do - manifest_path() - |> File.read!() - |> Jason.decode!() - end - - defp manifest_path do - project_manifest = Path.join(__DIR__, "../../manifest.json") |> Path.expand() - - candidates = - case :code.priv_dir(:my_plugin) do - {: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 - - defp migrations_path(manifest) do - if manifest["migrations"] do - candidates = - case :code.priv_dir(:my_plugin) do - {:error, :bad_name} -> - [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 - - defp first_existing_path(paths) do - Enum.find(paths, &File.exists?/1) + }) end end diff --git a/mix.exs b/mix.exs index 576208f..0620aed 100644 --- a/mix.exs +++ b/mix.exs @@ -26,15 +26,17 @@ defmodule MyPlugin.MixProject 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: - {:tribes, path: "../tribes", only: [:dev, :test], runtime: false}, + {:tribes_plugin_api, path: "../tribes/tribes_plugin_api", runtime: false}, # - # For CI or standalone development (when not co-located with tribes): - # {:tribes, github: "your-org/tribes", branch: "master", only: [:dev, :test]}, - - {:jason, "~> 1.2"} + # For CI or standalone development, this can be replaced with a published + # package once tribes_plugin_api is released. + # {:tribes_plugin_api, github: "your-org/tribes", sparse: "tribes_plugin_api"}, + {:phoenix, "~> 1.8"}, + {:phoenix_html, "~> 4.1"}, + {:phoenix_live_view, "~> 1.1.0"} ] end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..a6efb60 --- /dev/null +++ b/mix.lock @@ -0,0 +1,13 @@ +%{ + "mime": {:hex, :mime, "2.0.7", "b8d739037be7cd402aee1ba0306edfdef982687ee7e9859bee6198c1e7e2f128", [:mix], [], "hexpm", "6171188e399ee16023ffc5b76ce445eb6d9672e2e241d2df6050f3c771e80ccd"}, + "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_html": {:hex, :phoenix_html, "4.3.0", "d3577a5df4b6954cd7890c84d955c470b5310bb49647f0a114a6eeecc850f7ad", [:mix], [], "hexpm", "3eaa290a78bab0f075f791a46a981bbe769d94bc776869f4f3063a14f30497ad"}, + "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"}, + "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_crypto": {:hex, :plug_crypto, "2.1.1", "19bda8184399cb24afa10be734f84a16ea0a2bc65054e23a62bb10f06bc89491", [:mix], [], "hexpm", "6470bce6ffe41c8bd497612ffde1a7e4af67f36a15eea5f921af71cf3e11247c"}, + "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, + "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"}, +} diff --git a/test/contract_test.exs b/test/contract_test.exs index 22b476a..d8a6916 100644 --- a/test/contract_test.exs +++ b/test/contract_test.exs @@ -21,7 +21,7 @@ defmodule MyPlugin.ContractTest do @manifest_path Path.join(__DIR__, "../manifest.json") |> Path.expand() setup_all do - manifest = @manifest_path |> File.read!() |> Jason.decode!() + manifest = @manifest_path |> File.read!() |> JSON.decode!() spec = @plugin.register(%{pubsub: nil, repo: nil}) %{manifest: manifest, spec: spec} end diff --git a/test/my_plugin/manifest_test.exs b/test/my_plugin/manifest_test.exs index 93e8a60..1e37c5a 100644 --- a/test/my_plugin/manifest_test.exs +++ b/test/my_plugin/manifest_test.exs @@ -5,7 +5,7 @@ defmodule MyPlugin.ManifestTest do setup_all do content = File.read!(@manifest_path) - manifest = Jason.decode!(content) + manifest = JSON.decode!(content) %{manifest: manifest} end