Files
self a3ab1c5470 dev: generate en_GB locale archive in plugin devenvs
Set generated plugin devenv shells to use a Nix glibc locale archive containing en_GB.UTF-8, and apply the same setting to this repository.
2026-06-02 00:01:41 +02:00

1959 lines
59 KiB
Elixir

defmodule TribesPlugin.Templates do
@moduledoc false
alias TribesPlugin.Naming
alias TribesPlugin.Options
def project_files(%Naming{} = naming, opts) do
opts =
opts
|> Keyword.put_new(:description, "TODO: Describe what this plugin does")
|> Keyword.put_new(:host_path, "../tribes")
|> Keyword.put_new(:repo_name, naming.app)
|> Keyword.put_new(:live_view?, true)
|> Keyword.put_new(:assets?, true)
|> Keyword.put_new(:config_demo?, false)
|> Keyword.update(:provides, [], &Options.parse_capabilities/1)
|> Keyword.update(:enhances_with, [], &Options.parse_capabilities/1)
|> Options.with_default_requires()
base_files(naming, opts)
|> maybe_merge(opts[:assets?], asset_files(naming))
|> maybe_merge(opts[:live_view?], live_view_files(naming))
|> maybe_merge(opts[:live_view?], page_test_files(naming))
end
def page_files(%Naming{} = naming, page_name) do
page_module = Naming.page_module(page_name)
page_path = Naming.page_path(page_name)
%{
"lib/#{naming.app}_web/live/#{page_path}_live.ex" =>
page_live(naming, page_module, page_path),
"test/#{naming.app}/#{page_path}_page_test.exs" => page_test(naming, page_module, page_path)
}
end
defp base_files(naming, opts) do
%{
".credo.exs" => credo(),
".envrc" => envrc(),
".formatter.exs" => formatter(),
".github/workflows/ci.yml" => ci_yml(),
".gitignore" => gitignore(),
"AGENTS.md" => agents(),
"README.md" => readme(naming, opts),
"config/config.exs" => config_exs(naming),
"config/dev.exs" => dev_config(),
"config/prod.exs" => env_config(),
"config/test.exs" => host_config(opts),
"docs/checklist.md" => checklist(),
"docs/development.md" => development_doc(naming, opts),
"docs/plugin-contract.md" => plugin_contract(naming),
"lib/#{naming.app}/application.ex" => application_module(naming),
"lib/#{naming.app}/plugin.ex" => plugin_module(naming, opts),
"manifest.json" => manifest(naming, opts),
"mix.exs" => mix_exs(naming, opts),
"devenv.nix" => devenv_nix(opts),
"devenv.yaml" => devenv_yaml(),
"priv/repo/migrations/.gitkeep" => "",
"scripts/plugin" => plugin_script(),
"test/test_helper.exs" => test_helper(),
"test/#{naming.app}/plugin_contract_test.exs" => contract_test(naming)
}
end
defp asset_files(naming) do
%{
"assets/.npmrc" => npmrc(),
"assets/css/#{naming.app}.css" => css(naming),
"assets/package-lock.json" => package_lock(naming),
"assets/package.json" => package_json(naming),
"assets/ts/#{naming.app}.ts" => ts(naming),
"assets/tsconfig.json" => tsconfig(),
"priv/static/.gitkeep" => ""
}
end
defp live_view_files(naming) do
%{"lib/#{naming.app}_web/live/home_live.ex" => home_live(naming)}
end
defp page_test_files(naming) do
%{"test/#{naming.app}/home_page_test.exs" => home_page_test(naming)}
end
defp maybe_merge(files, true, more), do: Map.merge(files, more)
defp maybe_merge(files, false, _more), do: files
defp formatter do
"""
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
"""
end
defp envrc do
"""
#!/usr/bin/env bash
export DIRENV_WARN_TIMEOUT=20s
eval "$(devenv direnvrc)"
use devenv
"""
end
defp devenv_yaml do
"""
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
inputs:
nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling
git-hooks:
url: github:cachix/git-hooks.nix
inputs:
nixpkgs:
follows: nixpkgs
"""
end
defp devenv_nix(opts) do
js_env =
if opts[:assets?] do
"""
NODE_ENV = "development";
# Delay npm dependency resolution to reduce rushed supply-chain updates.
NPM_CONFIG_MIN_RELEASE_AGE = "7";
"""
else
"""
# If this plugin adds npm-managed assets later, enable languages.javascript
# below and set NPM_CONFIG_MIN_RELEASE_AGE = "7".
"""
end
js_language =
if opts[:assets?] do
"""
javascript = {
enable = true;
package = pkgs.nodejs_24;
npm.enable = true;
};
"""
else
"""
# javascript = {
# enable = true;
# package = pkgs.nodejs_24;
# npm.enable = true;
# };
"""
end
node_enter =
if opts[:assets?] do
"""
echo -n "Node.js "
node --version
"""
else
""
end
"""
{
pkgs,
lib,
...
}: let
devLocales = pkgs.glibcLocales.override {
allLocales = false;
locales = [
"en_GB.UTF-8/UTF-8"
"en_US.UTF-8/UTF-8"
];
};
in {
env = {
LANG = "en_GB.UTF-8";
LOCALE_ARCHIVE = "${devLocales}/lib/locale/locale-archive";
MIX_OS_DEPS_COMPILE_PARTITION_COUNT = 8;
#{js_env} };
packages = with pkgs;
[
git
alejandra
prettier
]
++ lib.optionals pkgs.stdenv.isLinux [
inotify-tools
];
languages = {
elixir = {
enable = true;
package = pkgs.elixir_1_19;
};
#{js_language} };
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
#{node_enter} echo
'';
scripts = {
plugin.exec = ''bash "$DEVENV_ROOT/scripts/plugin" "$@"'';
};
}
"""
end
defp ci_yml do
"""
name: CI
on:
push:
branches: [main, master]
pull_request:
branches: [main, master]
jobs:
test:
name: Test
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
MIX_ENV: test
steps:
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
elixir-version: "1.19"
otp-version: "27"
- name: Cache deps
uses: actions/cache@v4
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ hashFiles('mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Install dependencies
run: mix deps.get
- name: Compile (warnings as errors)
run: mix compile --warnings-as-errors
- name: Check formatting
run: mix format --check-formatted
- name: Run tests
run: scripts/plugin test
- name: Validate manifest
run: |
elixir -e '
manifest = "manifest.json" |> File.read!() |> JSON.decode!()
required = ["id", "slug", "version", "entry_module", "host_api", "provides", "requires"]
missing = Enum.filter(required, &(not Map.has_key?(manifest, &1)))
if missing != [] do
IO.puts(:stderr, "Missing manifest fields: \#{inspect(missing)}")
System.halt(1)
end
IO.puts("Plugin: \#{manifest["id"]} (\#{manifest["slug"]}) v\#{manifest["version"]}")
'
"""
end
defp credo do
"""
%{
configs: [
%{
name: "default",
files: %{
included: ["lib/", "src/", "test/"],
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
},
plugins: [],
requires: [],
strict: false,
parse_timeout: 5000,
color: true,
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, []},
{Credo.Check.Design.AliasUsage, false},
{Credo.Check.Design.TagTODO, [exit_status: 2]},
{Credo.Check.Design.TagFIXME, []},
{Credo.Check.Readability.MaxLineLength, [priority: :low, max_length: 120]},
{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.Nesting, [max_nesting: 7]},
{Credo.Check.Warning.IoInspect, []},
{Credo.Check.Warning.LazyLogging, false},
{Credo.Check.Warning.MixEnv, false},
{Credo.Check.Warning.UnsafeExec, []}
]
}
]
}
"""
end
defp gitignore do
"""
# Dependencies and build
/deps/
/_build/
/dist/
/result
/result-*
# Devenv
.devenv*
devenv.local.nix
# Pre-commit
.pre-commit-config.yaml
# Node
/node_modules/
/assets/node_modules/
# Built plugin assets
/priv/static/*.css
/priv/static/*.js
/priv/static/vendor/
# Browser tooling
.playwright-mcp/
# Generated on crash
erl_crash.dump
# Editor and OS
.elixir_ls/
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
Thumbs.db
# Env files
.env
.env.local
"""
end
defp agents do
"""
# Tribes Plugin Agent Guide
This repository is a Tribes plugin. Preserve generated names unless a task explicitly asks to rename the plugin.
## Required Workflow
- Use `plugin` for plugin-aware commands in the devenv shell: `plugin validate`, `plugin test`, `plugin precommit`, and `plugin smoke`. Outside the devenv shell, use the local `scripts/plugin` wrapper.
- 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.
- For AshPostgres resource changes, generate migrations with `plugin ash.codegen <name>`. Use `plugin ecto.migration <name>` only for rare manual schema migrations that are not derived from Ash resources.
- Do not edit an existing migration after it may have run; add a new migration instead.
- Run `scripts/plugin smoke` after changing `mix.exs`, `manifest.json`, entry modules, or host-facing dependency setup.
- Keep commits semantic, for example `feat: add plugin smoke checks`, and include a body except for minimal patches.
## Plugin Contract
- `manifest.json` `entry_module` must point at the plugin entry module.
- 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 `org.tribe-one.caps.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`.
- Register plugin LiveView hooks from `manifest.json` `assets.global_js` bundles via `window.TribesPluginHooks`. The host does not auto-import external plugin `phoenix-colocated/<otp_app>` modules into its `app.js` bundle.
- Keep CSS selectors plugin-scoped 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.
"""
end
defp readme(naming, opts) do
"""
# #{naming.title}
#{opts[:description]}
## Getting Started
1. Edit `manifest.json` - set description, capabilities, requirements
2. Implement your plugin in `lib/#{naming.app}/plugin.ex`
3. Run validation, smoke checks, and tests:
```bash
mix deps.get
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
```
Generate AshPostgres resource migrations through the same wrapper so they
target the host `Tribes.Repo` and the plugin migration directory:
```bash
plugin ash.codegen update_example_resources
```
Manual Ecto migrations are the rare exception for schema work not derived
from Ash resources:
```bash
plugin ecto.migration add_example_table
```
## Development
For local development alongside a Tribes checkout:
```bash
# Symlink into the host plugins directory once
cd #{opts[:host_path]}
ln -s ../#{opts[:repo_name]} plugins/#{naming.app}
# Start Tribes dev server
iex --sname dev -S mix phx.server
```
Then edit the plugin in its own repo. In development, the host 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 and plugin asset files change
- reloads the plugin in the running Tribes VM
- triggers a Phoenix browser reload after the rebuild finishes
Browser hooks for external plugins should live in plugin JS assets declared
in `manifest.json` `assets.global_js` and register themselves through
`window.TribesPluginHooks`. Phoenix colocated hooks are not auto-imported
from external plugin OTP apps by the host `app.js` bundle.
That means the normal edit/compile loop is:
```bash
cd /path/to/#{opts[:repo_name]}
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 generator uses TypeScript under `assets/ts`.
## LiveView Hooks
External plugins do not participate in the host asset bundle, so the host
cannot statically import arbitrary plugin `phoenix-colocated/<otp_app>`
modules. Register browser hooks from your plugin JS instead:
```typescript
window.TribesPluginHooks = window.TribesPluginHooks ?? {};
window.TribesPluginHooks.#{String.replace(naming.module, ".", "")}Example = {
mounted() {
console.info("#{naming.app} hook mounted");
}
};
```
Keep the compiled JS file listed in `manifest.json` `assets.global_js` so it
loads before the host LiveSocket connects.
## Project Structure
```
#{naming.app}/
|-- manifest.json # Plugin metadata (Nix build + runtime)
|-- mix.exs # Dependencies
|-- config/ # Host-backed test config
|-- lib/
| |-- #{naming.app}/
| | |-- plugin.ex # Tribes.Plugin entry point
| | `-- application.ex # OTP supervision tree (optional)
| `-- #{naming.app}_web/
| `-- live/ # LiveView pages
|-- 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/
| |-- static/ # Built assets for release
| `-- repo/migrations/ # Ecto migrations
|-- scripts/
| `-- plugin # Shared devenv/non-devenv test wrapper
`-- test/
```
## Manifest
`manifest.json` declares your plugin's identity and capabilities:
```json
{
"id": "#{plugin_id(naming)}",
"slug": "#{naming.app}",
"display_name": "#{naming.title}",
"entry_module": "#{naming.entry_module}",
"host_api": "1",
"otp_app": "#{otp_app(naming)}",
"provides": ["org.tribe-one.caps.some_capability@1"],
"requires": ["org.tribe-one.caps.ui@1"],
"enhances_with": ["org.tribe-one.caps.inference@1"]
}
```
- **entry_module** - must be a valid module ending in `.Plugin`
- **otp_app** - required OTP application name; vendor-prefix it to avoid collisions
- **provides** - capabilities this plugin makes available
- **requires** - hard plugin/API contracts (build fails without them)
- **enhances_with** - optional dependencies (plugin degrades gracefully)
The default generator includes `org.tribe-one.caps.ui@1` because the generated LiveView renders
inside the host chrome through `Tribes.Plugin.Layouts.app`. Keep `org.tribe-one.caps.ui@1` when a
plugin uses host chrome, imports `Tribes.UI.Components`, or uses `use Tribes.UI`.
## Plugin Data
If your plugin adds Ash resources that should replicate cluster-wide, use
`extensions: [AshNostrSync]` on those resources and assign a sync lane per
resource. The host default lane is `bulk`; latency-sensitive topology or
orchestration resources should opt into `control`. Keep resource-specific
side effects explicit in actions.
## Testing
The generated plugin starts with two host-backed test layers:
- **Contract tests** (`test/#{naming.app}/plugin_contract_test.exs`) - manifest and runtime spec stay aligned
- **Page tests** (`test/#{naming.app}/home_page_test.exs`) - the plugin renders through the real host page pipeline
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
```bash
MIX_ENV=prod mix compile
devenv shell -- npm run build --prefix assets
mkdir -p dist/#{naming.app}
cp -r _build/prod/lib/#{otp_app(naming)}/ebin dist/#{naming.app}/
cp -r priv dist/#{naming.app}/
cp manifest.json dist/#{naming.app}/
```
## Licence
TODO: Choose a licence.
"""
end
defp config_exs(_naming) do
"""
import Config
import_config "\#{config_env()}.exs"
"""
end
defp env_config, do: "import Config\n"
defp dev_config do
"""
import Config
# The host is a compile-only dependency in dev. Suppress host Ash domain
# config inclusion warnings while compiling host modules as a dependency.
config :ash, :validate_domain_config_inclusion?, false
"""
end
defp host_config(opts) do
host_config_path = Path.join(["..", opts[:host_path], "config/config.exs"])
"""
import Config
import_config #{inspect(host_config_path)}
"""
end
defp checklist do
"""
# Checklist
## After Generation
- Confirm `manifest.json` `id`, `slug`, `otp_app`, and `entry_module`.
- Confirm `manifest.json` `entry_module` matches your plugin module.
- 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.
"""
end
defp development_doc(naming, opts) do
"""
# Development
## Local Setup
This plugin 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 #{opts[:host_path]}
ln -s ../#{opts[:repo_name]} plugins/#{naming.app}
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 ash.codegen update_example_resources
scripts/plugin ecto.migration add_example_table
scripts/plugin smoke
```
Inside the plugin 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
```
"""
end
defp plugin_contract(naming) do
"""
# Plugin Contract
## Manifest
`manifest.json` is the runtime contract consumed by Tribes:
- `id` is the canonical reverse-DNS plugin identity; `slug` is the local URL/assets namespace.
- `otp_app` should be vendor-prefixed and must match the Mix application name.
- `entry_module` must be a loadable module ending in `.Plugin`.
- `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.
`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.
Declare `org.tribe-one.caps.ui@1` in `requires` when rendering through `Tribes.Plugin.Layouts.app`,
importing `Tribes.UI.Components`, or using `use Tribes.UI`.
## Entry Modules
The plugin entry module is:
```elixir
defmodule #{naming.entry_module} do
use Tribes.Plugin.Base, otp_app: :#{otp_app(naming)}
end
```
Keep plugin implementation code under your own namespace. The default
generator uses `TribeOne.TribesPlugin.*`; third-party plugins should use their own
organization or project namespace.
## 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.
Host services are exposed through public facade modules in
`Tribes.Plugin.Services`. Use `Tribes.Plugin.Services.Alliance` for local
tribe metadata and tribe users, `Tribes.Plugin.Services.Metrics` for compact
metric rollups, and `Tribes.Plugin.Services.Logs` for operational plugin
logging.
## Localization
Keep plugin-specific copy in this plugin's own Gettext backend and
`priv/gettext` catalogs. For shared Tribes UI labels that are documented as
public plugin API, import `Tribes.Plugin.Gettext` and call `tgettext/2`:
```elixir
import Tribes.Plugin.Gettext
tgettext("Cancel")
tgettext("Save")
```
Do not call arbitrary internal Tribes Gettext domains from plugin code.
The generated plugin 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.
"""
end
defp application_module(naming) do
"""
defmodule #{naming.module}.Application do
@moduledoc \"\"\"
OTP Application for this plugin.
Uncomment the `mod:` entry in mix.exs to activate this supervision tree.
Add plugin-specific GenServers, workers, or supervisors as children here.
\"\"\"
use Application
@impl true
def start(_type, _args) do
children = [
# Add your supervised processes here, e.g.:
# {#{naming.module}.Worker, []}
]
opts = [strategy: :one_for_one, name: #{naming.module}.Supervisor]
Supervisor.start_link(children, opts)
end
end
"""
end
defp plugin_module(naming, opts) do
entries =
if opts[:live_view?] do
"""
nav_items: [
%{
label: "#{naming.title}",
path: "#{naming.path}",
icon: nil,
requires: [],
order: 50
}
],
pages: [
%{
path: "#{naming.path}",
live_view: #{naming.web_module}.HomeLive,
layout: nil
}
],
"""
else
"""
nav_items: [],
pages: [],
"""
end
config_schema =
if opts[:config_demo?] do
"""
config_schema: %{
title: "#{naming.title}",
description: "Runtime defaults for #{naming.title}.",
scope: :tribe,
groups: [
%{
id: "defaults",
label: "Defaults",
settings: [
%{
key: "defaults.enabled",
label: "Enabled",
type: :boolean,
default: true
}
]
}
]
},
"""
else
""
end
"""
defmodule #{naming.module}.Plugin do
@moduledoc \"\"\"
Tribes plugin entry point.
\"\"\"
use Tribes.Plugin.Base, otp_app: :#{otp_app(naming)}
@impl true
def register(context) do
super(context)
|> Map.merge(%{
#{entries}#{config_schema} api_routes: [],
management_methods: [],
metrics: [],
scheduler_crons: [],
plugs: [],
hooks: %{}
})
end
end
"""
end
defp manifest(naming, opts) do
assets = manifest_assets(opts[:assets?], naming)
"""
{
"id": "#{plugin_id(naming)}",
"slug": "#{naming.app}",
"display_name": "#{naming.title}",
"version": "0.1.0",
"description": #{JSON.encode!(opts[:description])},
"entry_module": "#{naming.entry_module}",
"host_api": "1",
"otp_app": "#{otp_app(naming)}",
"provides": #{json_array(opts[:provides])},
"requires": #{json_array(opts[:requires])},
"enhances_with": #{json_array(opts[:enhances_with])},
"assets": {
"global_js": #{json_array(assets.global_js)},
"global_css": #{json_array(assets.global_css)}
},
"migrations": false,
"children": false
}
"""
end
defp manifest_assets(true, naming) do
%{global_js: ["#{naming.app}.js"], global_css: ["#{naming.app}.css"]}
end
defp manifest_assets(false, _naming), do: %{global_js: [], global_css: []}
defp json_array(values) do
values
|> Enum.map(&JSON.encode!/1)
|> Enum.join(", ")
|> then(&"[#{&1}]")
end
defp plugin_id(naming), do: "org.tribe-one.plugins." <> naming.css_class
defp otp_app(naming), do: "tribe_one_" <> naming.app
defp mix_exs(naming, opts) do
host_path = opts[:host_path]
"""
defmodule #{naming.module}.MixProject do
use Mix.Project
def project do
[
app: :#{otp_app(naming)},
version: "0.1.0",
elixir: "~> 1.18",
elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod,
deps: deps(),
aliases: aliases(),
usage_rules: usage_rules()
]
end
def cli do
[
preferred_envs: [precommit: :test, raw_precommit: :test, raw_test: :test]
]
end
def application do
[
extra_applications: [:logger]
# Uncomment if your plugin needs its own supervision tree:
# mod: {#{naming.module}.Application, []}
]
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
defp deps do
[
# Plugin API dependency for local development alongside a tribes checkout.
#
# For CI or standalone development, this can be replaced with a published
# package once tribes_plugin_api is released.
{:tribes_plugin_api, path: "#{host_path}/tribes_plugin_api", runtime: false},
{:tribes_plugin, path: "../tribes-plugin-new", only: [:dev, :test], runtime: false},
{: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
defp tribes_deps(:dev), do: [{:tribes, path: "#{host_path}", only: :dev, runtime: false}]
defp tribes_deps(:test), do: [{:tribes, path: "#{host_path}", 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
defp aliases do
[
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
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
defp plugin_script do
~S"""
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
plugin validate
plugin test [mix test args...]
plugin precommit [mix precommit args...]
plugin ecto.migration <name> [mix ecto.gen.migration args...]
plugin ash.codegen <name> [mix ash_postgres.generate_migrations 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 | ecto.migration | ash.codegen | 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" "$@"
"""
end
defp test_helper, do: "ExUnit.start()\n"
defp contract_test(naming) do
"""
defmodule #{naming.module}.PluginContractTest do
use Tribes.PluginTest.ContractTest, plugin: #{naming.entry_module}
end
"""
end
defp home_live(naming) do
"""
defmodule #{naming.web_module}.HomeLive do
@moduledoc \"\"\"
Example LiveView page for the plugin.
This page is registered in the plugin spec, mounted by the host at
#{naming.path}, and rendered inside the Tribes chrome by default.
\"\"\"
# In dev mode (plugin loaded as path dep), you can use host macros:
# use TribesWeb, :live_view
#
# For release builds (standalone OTP app), use Phoenix.LiveView directly:
use Phoenix.LiveView
on_mount({Tribes.Plugin.LiveUserAuth, :live_user_optional})
alias Tribes.Plugin.Layouts
import Tribes.Plugin.Gettext
def mount(_params, _session, socket) do
{:ok, assign(socket, :page_title, "#{naming.title}")}
end
def render(assigns) do
~H\"\"\"
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="#{naming.css_class} p-6">
<h1 class="text-2xl font-bold">#{naming.title}</h1>
<p class="mt-2 text-base-content/70">
{tgettext("This is a Tribes plugin.")} Edit this page in
<code>lib/#{naming.app}_web/live/home_live.ex</code>.
</p>
</div>
</Layouts.app>
\"\"\"
end
end
"""
end
defp home_page_test(naming) do
"""
defmodule #{naming.module}.HomePageTest do
use Tribes.PluginTest.PageCase, plugin: #{naming.entry_module}
test "renders the plugin home page for signed-out visitors", %{conn: conn} do
{:ok, view, html} = live(conn, "#{naming.path}")
assert html =~ "#{naming.title}"
assert html =~ "This is a Tribes plugin."
assert has_element?(view, "#plugin-nav-#{naming.css_class}", "#{naming.title}")
end
test "dispatches top-level subpaths to the plugin page", %{conn: conn} do
{:ok, _view, html} = live(conn, "#{naming.path}/example")
assert html =~ "#{naming.title}"
end
end
"""
end
defp page_live(naming, page_module, page_path) do
title = page_title(page_module)
"""
defmodule #{naming.web_module}.#{page_module} do
@moduledoc false
use Phoenix.LiveView
on_mount({Tribes.Plugin.LiveUserAuth, :live_user_optional})
alias Tribes.Plugin.Layouts
def mount(_params, _session, socket) do
{:ok, assign(socket, :page_title, "#{title}")}
end
def render(assigns) do
~H\"\"\"
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="#{naming.css_class} p-6" id="#{naming.css_class}-#{page_path}-page">
<h1 class="text-2xl font-bold">#{title}</h1>
</div>
</Layouts.app>
\"\"\"
end
end
"""
end
defp page_test(naming, page_module, page_path) do
title = page_title(page_module)
"""
defmodule #{naming.module}.#{page_module}PageTest do
use Tribes.PluginTest.PageCase, plugin: #{naming.entry_module}
test "renders #{page_path}", %{conn: conn} do
{:ok, _view, html} = live(conn, "#{naming.path}/#{page_path}")
assert html =~ "#{title}"
end
end
"""
end
defp page_title(page_module) do
page_module
|> String.trim_trailing("Live")
|> Macro.underscore()
|> String.replace("_", " ")
|> String.capitalize()
end
defp css(naming) do
"""
/*
* Plugin CSS entry point.
*
* Served at /plugins-assets/#{naming.app}/#{naming.app}.css
* and included in the page layout if declared in manifest.json assets.global_css.
*
* Prefix all selectors with your plugin name to avoid collisions
* with host or other plugin styles.
*/
.#{naming.css_class} {
/* Plugin-scoped styles go here */
}
"""
end
defp ts(naming) do
"""
// Plugin TypeScript entry point.
//
// This file is compiled to /priv/static/#{naming.app}.js, served by the host at
// /plugins-assets/#{naming.app}/#{naming.app}.js, and included when declared in
// manifest.json assets.global_js.
//
// Register plugin LiveView hooks here. External plugin OTP apps are not
// auto-imported by the host's phoenix-colocated bundle.
declare global {
interface Window {
TribesPluginHooks?: Record<string, unknown>;
}
}
window.TribesPluginHooks = window.TribesPluginHooks ?? {};
// Example:
// window.TribesPluginHooks.#{String.replace(naming.module, ".", "")}Example = {
// mounted() {
// console.info("#{naming.app} hook mounted");
// },
// };
console.info("#{naming.app} loaded");
export {};
"""
end
defp npmrc do
"""
min-release-age=7
"""
end
defp package_json(naming) do
"""
{
"name": "#{naming.css_class}-assets",
"private": true,
"version": "0.1.0",
"scripts": {
"build": "npm run check && mkdir -p ../priv/static && cp -r css/. ../priv/static && esbuild ts/#{naming.app}.ts --bundle --target=es2022 --format=iife --outfile=../priv/static/#{naming.app}.js",
"check": "tsc --project tsconfig.json --noEmit"
},
"devDependencies": {
"esbuild": "^0.28.0",
"typescript": "^6.0.3"
}
}
"""
end
defp package_lock(naming) do
"""
{
"name": "#{naming.css_class}-assets",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "#{naming.css_class}-assets",
"version": "0.1.0",
"devDependencies": {
"esbuild": "^0.28.0",
"typescript": "^6.0.3"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/esbuild": {
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.0"
}
},
"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"
}
}
}
}
"""
end
defp tsconfig do
"""
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "Bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"skipLibCheck": true,
"outDir": "../priv/static",
"rootDir": "ts"
},
"include": ["ts/**/*.ts"]
}
"""
end
end