A host installs a mountable library with one command:
mix igniter.install my_lib
Igniter adds the dependency, then runs the library's installer, which edits the host's code safely and shows each change as a diff before it writes. Every step is idempotent: running the installer again changes nothing.
In one use
PhoenixMountable.Install declares the installer:
defmodule Mix.Tasks.MyLib.Install do
use PhoenixMountable.Install,
otp_app: :my_lib,
mounts: [
{MyLib.Router, "/my_lib", scope: "/admin", options: "resolver: {web}.MyLibResolver"}
],
migrations: MyLib.Migrations,
hook: {"MyLib", "my_lib"},
css: ["my_lib.css", {"my_lib-dark.css", :comment}],
manual: "See https://hexdocs.pm/my_lib/installation.html"
endThe steps run in this order, each when its option is given:
| Option | Step |
|---|---|
:mounts | mounts each definition in the router: in a scope (:scope, "/" by default) with pipelines (:pipelines, [:browser]), with options ({web} is the host's web module) |
:migrations | writes the migration that installs or upgrades the library's tables |
:hook | adds import { MyLib } from "my_lib" and the hook to the LiveSocket of assets/js/app.js |
:css | imports the library's style sheets in assets/css/app.css, a theme as a comment |
:tailwind | adds the library to the host's Tailwind sources |
:supervisor | starts the mounts' processes at boot |
Then the library's own steps:
@impl PhoenixMountable.Install
def install(igniter) do
igniter
|> Igniter.Project.Application.add_new_child({MyLib, repo: MyApp.Repo})
|> Igniter.Project.Module.create_module(MyAppWeb.MyLibResolver, "...")
endIgniter stays an optional dependency of the library. In a host without
it, mix my_lib.install says how to get it, and shows :manual.
The building blocks
PhoenixMountable.Igniter has each step as a function, for an
installer written by hand:
defmodule Mix.Tasks.MyLib.Install do
use Igniter.Mix.Task
@impl Igniter.Mix.Task
def info(_argv, _parent), do: %Igniter.Mix.Task.Info{group: :my_lib}
@impl Igniter.Mix.Task
def igniter(igniter) do
igniter
|> PhoenixMountable.Igniter.mount(MyLib.Router, "/my_lib")
|> PhoenixMountable.Igniter.gen_migration(MyLib.Migrations)
|> PhoenixMountable.Igniter.add_tailwind_source(:my_lib)
end
endmount/4adds the import and the mount call to the router's scope with the given path and pipelines, and the library to the host's.formatter.exs. A library whose several definitions are re-exported by one module passesimport: MyLib.Router.gen_migration/3writes the migration from the version the host's migrations pin to the latest, or nothing when the host is up to date, with Igniter's own migration generator. Call it from the library's upgrader too.add_hook/4andadd_css_imports/4editapp.jsandapp.css, for a library whose components render in the host's own pages (a button in its header, a palette). The edits arePhoenixMountable.Igniter.HostAssets; whereapp.jscannot be edited safely, the host gets the lines to add.add_supervisor/2starts the mounts' processes at boot.add_tailwind_source/2adds the@source(Tailwind v4) orcontent(Tailwind v3) line for libraries styled by the host.
Mix tasks
mix phoenix_mountable.gen.migration MyLib.Migrationswrites the install or upgrade migration. A library wraps it in its own task.mix phoenix_mountable.checkchecks every library's schema, for CI.mix phoenix_mountable.backfill MyLib.Migrationsruns deferred backfills.
Formatting
Export the mount macro in the library's .formatter.exs, so that a host
that imports the library's formatting (mount/4 adds it) writes
my_lib "/my_lib" without parentheses:
[
import_deps: [:phoenix_mountable],
export: [locals_without_parens: [my_lib: 1, my_lib: 2]]
]import_deps: [:phoenix_mountable] formats the definition's DSL
(routes, live, options).
Testing an installer
Igniter.Test.test_project/1 with the files the installer edits (a
router, assets/js/app.js, assets/css/app.css) runs it without a real
app, and without the phx_new archive that phx_test_project/1 needs:
igniter =
Igniter.Test.test_project(files: %{"lib/test_web/router.ex" => router, "assets/js/app.js" => app_js})
|> Igniter.compose_task(Mix.Tasks.MyLib.Install, [])