Packages
mob_dev
0.6.14
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.37
0.3.35
0.3.34
0.3.33
0.3.28
0.3.26
0.3.23
0.3.21
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.18
0.2.17
0.2.15
0.2.14
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.0
Development tooling for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mob_dev/plugin/runtime_manifest.ex
defmodule MobDev.Plugin.RuntimeManifest do
@moduledoc """
Builds the host's **runtime plugin manifest** — the device-readable record of
the activated plugins' tier-3/4 contributions.
Tiers 3 and 4 are pure-Elixir and runtime-wired: the host needs to know, on
device, which screens exist, what lifecycle/settings/notification declarations
each plugin made — but `MobDev.Plugin.activated/0` is compile-time only. So at
build time this module gathers those sections (running any spec-v2
`:screens_generator` under the host-config audit) and emits a terms file
(`priv/generated/mob_plugins.exs`) that the core `Mob.Plugins` module reads at
boot. It mirrors the `driver_tab` / `MobPluginBootstrap` codegen pattern, but
for serializable Elixir data instead of native symbols.
Only the behavioral data lives here. Migration files and font/image assets are
physically copied into the host at build time (see `native_build`), not carried
in this manifest.
Everything emitted must be serializable terms — atoms, tuples, maps, strings,
integers. The manifest validator forbids closures in plugin sections so this
always holds.
"""
alias MobDev.Plugin.Merge
@doc """
Builds the merged runtime manifest map from `{plugin_dir, manifest}` pairs.
Static `:screens` and spec-v2 `:screens_generator` output are combined (each
tagged with its plugin); generators run under `MobDev.Plugin.with_host_config_audit/3`
so an undeclared host-config read fails the build.
"""
@spec build([{Path.t(), map() | nil}]) :: map()
def build(plugins) do
# Styles ride the same runtime manifest (MOB_STYLES.md shares the plugin
# infrastructure): the activated token-only style packages + the
# configured default, applied by core at boot (Mob.Plugins.apply_default_style).
%{styles: styles, default_style: default_style} = MobDev.Style.runtime_entries!()
%{
screens: Merge.screens(plugins) ++ generated_screens(plugins),
lifecycle: Merge.lifecycle(plugins),
settings: Merge.settings(plugins),
notification_handlers: Merge.notification_handlers(plugins),
nifs: nif_modules(plugins),
composites: composites(plugins),
styles: styles,
default_style: default_style
}
end
# Pure-Elixir composite components (the ui_components expand: form) — core
# registers each into Mob.Composite at boot.
defp composites(plugins) do
for c <- Merge.ui_components(plugins),
match?({m, f} when is_atom(m) and is_atom(f), c[:expand]) do
%{plugin: c[:plugin], atom: c[:atom], expand: c[:expand]}
end
end
# The activated plugins' NIF module atoms (deduped, platform-agnostic — the
# same module name backs both the iOS and Android NIF). Core loads these at
# boot so an iOS plugin NIF's `load` callback fires eagerly, registering any
# permission handler it owns before a screen can request that permission.
# (Android registers permissions eagerly via MobPluginBootstrap, so this is
# the iOS counterpart; loading on both platforms is harmless and fail-fast.)
@spec nif_modules([{Path.t(), map() | nil}]) :: [atom()]
defp nif_modules(plugins) do
Merge.nifs(plugins)
|> Enum.map(& &1[:module])
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
end
defp generated_screens(plugins) do
for {_dir, manifest} <- plugins,
is_map(manifest),
gen = manifest[:screens_generator],
not is_nil(gen) do
{m, f, a} = gen
allowed = manifest[:host_config_keys] || []
name = manifest[:name]
{screens, _reads} =
MobDev.Plugin.with_host_config_audit(name, allowed, fn -> apply(m, f, a) end)
screens
|> validate_generated_screens(name)
|> Enum.map(&Map.put(&1, :plugin, name))
end
|> List.flatten()
end
@doc """
Validates a `:screens_generator`'s output, raising on malformed specs.
A generator must return a list of `%{module: atom, default_route: binary}`
maps (a bare map is wrapped). This mirrors `MobDev.Plugin.Manifest`'s static
`:screens` validation so generator-produced screens are held to the same
shape contract — otherwise a typo'd or wrong-typed spec would be written into
`mob_plugins.exs` and silently dropped at boot (`Mob.Plugins.register_screens/0`
pattern-matches `%{module:, default_route:}` and skips non-matching maps),
making the plugin's screen vanish with no build-time error.
Returns the validated screen list (without the `:plugin` tag, which the caller
adds). Public for testing.
"""
@spec validate_generated_screens(term(), atom() | nil) :: [map()]
def validate_generated_screens(screens, plugin_name) do
screens
|> List.wrap()
|> Enum.with_index()
|> Enum.map(fn {entry, i} -> validate_screen_entry(entry, i, plugin_name) end)
end
defp validate_screen_entry(%{module: m, default_route: r} = entry, _i, _plugin)
when is_atom(m) and not is_nil(m) and is_binary(r),
do: entry
defp validate_screen_entry(entry, i, plugin) do
raise ArgumentError,
"plugin #{inspect(plugin)} :screens_generator produced an invalid screen at " <>
"index #{i}: expected a %{module: atom, default_route: binary} map, got: " <>
"#{inspect(entry)}. Fix the generator to emit valid screen specs."
end
@doc """
Renders the manifest map to the contents of `priv/generated/mob_plugins.exs` —
a self-describing `.exs` that evaluates back to the map.
"""
@spec render(map()) :: String.t()
def render(manifest) do
"""
# Generated by `mix mob.regen_plugin_manifest` — do not edit by hand.
#
# The activated plugins' tier-3/4 contributions, read at boot by Mob.Plugins.
# Regenerated whenever `config :mob, :plugins` changes (the deploy/regen hook).
#{inspect(manifest, limit: :infinity, printable_limit: :infinity, pretty: true)}
"""
end
@doc """
Writes the rendered manifest to `<host_root>/priv/generated/mob_plugins.exs`
and returns the path.
"""
@spec write(Path.t(), map()) :: Path.t()
def write(host_root, manifest) do
path = Path.join([host_root, "priv", "generated", "mob_plugins.exs"])
File.mkdir_p!(Path.dirname(path))
File.write!(path, render(manifest))
path
end
end