PhoenixMountable

PhoenixMountable — Mountable libraries for Phoenix

PhoenixMountable is for building libraries that mount into a Phoenix app, the way Phoenix LiveDashboard, Oban Web and Lotus Web do. A host app adds one line to its router:

import MyLib.Router

scope "/" do
  pipe_through :browser
  my_lib "/my_lib", resolver: MyApp.MyLibResolver
end

and gets the library's pages, controllers, assets, processes and tables. It gives your library:

LiveView is optional: a library of controllers, a JSON API or a single page app mounts the same way. The migrations and processes work without Phoenix at all, so a library split in two packages, like lotus and lotus_web, uses them in its core and the mounting in its web package.

Installation

def deps do
  [
    {:phoenix_mountable, "~> 0.2"}
  ]
end

PhoenixMountable needs Elixir 1.18 or later, Phoenix 1.7 or later and, for live routes, Phoenix LiveView 1.1 or later.

A mountable library

The definition module declares everything a host gets:

defmodule MyLib.Router do
  use PhoenixMountable,
    otp_app: :my_lib,
    macro: :my_lib,
    layout: {MyLib.Layouts, :root},
    on_mount: [MyLib.Locale],
    schema: MyLib.Migrations,
    assets:
      {PhoenixMountable.Assets.Embedded,
       css: "priv/static/app.css",
       js: "priv/static/app.js",
       prepend: [:phoenix, :phoenix_html, :phoenix_live_view, :phoenix_mountable]}

  options resolver: [
            type: :atom,
            default: MyLib.Resolver,
            verify: {:behaviour, MyLib.Resolver},
            doc: "Resolves the current user and what they may do."
          ],
          features: [type: {:list, :atom}, default: []]

  routes do
    get "/export", MyLib.ExportController, :csv, as: :export

    live_session :public, host_on_mount: false do
      live "/share/:token", MyLib.ShareLive, :show
    end

    live_session :default, on_mount: [MyLib.Auth] do
      live "/", MyLib.HomeLive, :index
      live "/items/:id", MyLib.ItemLive, :show
    end
  end

  @impl PhoenixMountable
  def session(conn, mount, _session) do
    %{"user" => mount.options.resolver.resolve_user(conn)}
  end

  @impl PhoenixMountable
  def children(mount) do
    [{MyLib.Cache, name: PhoenixMountable.via(mount, :cache)}]
  end

  @impl PhoenixMountable
  def repo(_mount), do: Application.fetch_env!(:my_lib, :repo)
end

A host mounts it with my_lib "/my_lib". Every mount macro also takes :as (to mount the library twice), :on_mount, :socket_path, :transport, :csp_nonce_assign_key, :asset_host and :pubsub.

In the library's LiveViews and templates:

use PhoenixMountable.Path, mountable: MyLib.Router

~H"""
<.link navigate={~m"/items/#{@item}"}>{@item.name}</.link>
<p>Mounted at {@mount.path}, with {inspect(@mount.options.features)}</p>
"""

Guides

How it compares

PhoenixMountable A router macro by hand (LiveDashboard, Oban Web) Rails engines
Routes in the host ordinary Phoenix routes, generated written by hand per library a mounted Rack app
Options validated at compile time, documented from their schema validated by hand
Assets embedded, static or the host's, with deploy reloads embedded, by hand the asset pipeline
Processes per mount, lazy or at boot, singletons
Migrations versioned, tracked per prefix on three databases, with backfills and checks Oban: by hand, Postgres comments copied into the app

License

MIT. See LICENSE.