You've already forked tribes-plugin-new
a3ab1c5470
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.
1959 lines
59 KiB
Elixir
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
|