Packages
mob_dev
0.6.11
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/adopt_guard.ex
defmodule MobDev.AdoptGuard do
@moduledoc false
# Pre-1.0 detect-and-refuse for `mix mob.adopt`. Adds `Igniter.add_issue/2`
# entries when the target project doesn't match the blessed shape. The
# caller is expected to skip the rest of its work when `igniter.issues`
# is non-empty after `check/2` runs.
alias Igniter.Project.Application, as: ProjectApplication
alias Igniter.Project.Deps, as: ProjectDeps
@doc """
Returns `:live_view` when LV bridge mode is in effect (default) or
`:thin` when `--no-live-view` was passed.
"""
@spec mode_from(keyword()) :: :live_view | :thin
def mode_from(opts) do
if Keyword.get(opts, :live_view, true), do: :live_view, else: :thin
end
@doc """
Runs the blessed-shape checks for `mode`. Returns the igniter with
`add_issue/2` entries appended for any check that fails. Caller gates
on `igniter.issues == []`.
"""
@spec check(Igniter.t(), :live_view | :thin) :: Igniter.t()
def check(igniter, mode) do
igniter
|> refuse_if_umbrella()
|> require_phoenix_dep()
|> maybe_check_live_view_shape(mode)
end
defp refuse_if_umbrella(igniter) do
if umbrella?(igniter) do
Igniter.add_issue(igniter, """
mob.adopt does not support umbrella applications.
Run it from inside one of the sub-app folders instead.
""")
else
igniter
end
end
# Stubbable via `Igniter.assign(:umbrella?, true|false)` for tests.
defp umbrella?(igniter) do
case igniter.assigns[:umbrella?] do
nil -> Mix.Project.umbrella?()
bool -> bool
end
end
defp require_phoenix_dep(igniter) do
if ProjectDeps.has_dep?(igniter, :phoenix) do
igniter
else
Igniter.add_issue(igniter, """
mob.adopt requires a Phoenix project (`:phoenix` in your mix.exs deps).
For non-Phoenix Elixir apps, follow the manual install path documented
at `mix help mob.adopt`.
""")
end
end
defp maybe_check_live_view_shape(igniter, :thin), do: igniter
defp maybe_check_live_view_shape(igniter, :live_view) do
igniter
|> check_app_js()
|> check_root_html()
|> check_repo_shape()
end
# The LV-flavoured `mob_app.ex` calls `Application.ensure_all_started(:ecto_sqlite3)`
# and runs `Ecto.Migrator.run(<App>.Repo, ...)` on-device. That assumes
# the host has an Ecto Repo using the SQLite adapter (the `mix mob.new`
# shape). Refuse loudly when the host doesn't match — silently emitting
# a mob_app.ex that tries to migrate Postgres on a phone would crash at
# boot.
defp check_repo_shape(igniter) do
cond do
not has_any_ecto_repo?(igniter) ->
Igniter.add_issue(igniter, """
mob.adopt (LiveView mode) generates a `mob_app.ex` that boots Ecto
and runs migrations on-device. Your project has no Ecto Repo
(no `:ecto_sql` in deps).
Options:
- Add an Ecto Repo before adopting (e.g. start from a phx.new
project with `--database sqlite3`).
- Or use `--no-live-view` for the thin-client path — the phone
opens a deployed Phoenix server; no on-device DB needed.
""")
has_non_sqlite_adapter?(igniter) and not has_sqlite_adapter?(igniter) ->
Igniter.add_issue(igniter, """
mob.adopt (LiveView mode) generates a `mob_app.ex` that migrates the
host's `<App>.Repo` on-device — assumes SQLite. Your project looks
like it uses Postgres / MySQL / MSSQL, which won't run on a phone.
Options:
- Use `--no-live-view` for the thin-client path (server hosts
Phoenix + your existing DB; phone is just a WebView shell).
- Switch the host Repo to SQLite (matches `mix mob.new --liveview`).
- Wait for the upcoming `--with-local-repo` mode that generates a
separate SQLite LocalRepo + target-aware Repo selection.
""")
true ->
igniter
end
end
defp has_any_ecto_repo?(igniter), do: ProjectDeps.has_dep?(igniter, :ecto_sql)
defp has_sqlite_adapter?(igniter), do: ProjectDeps.has_dep?(igniter, :ecto_sqlite3)
defp has_non_sqlite_adapter?(igniter) do
Enum.any?([:postgrex, :myxql, :tds], &ProjectDeps.has_dep?(igniter, &1))
end
defp check_app_js(igniter) do
path = "assets/js/app.js"
cond do
not Igniter.exists?(igniter, path) ->
Igniter.add_issue(igniter, """
mob.adopt (LiveView mode) requires #{path}. Not found.
Use `--no-live-view` for thin-client mode (WebView opens a remote URL;
no app.js patches needed).
""")
not stock_live_socket?(igniter, path) ->
Igniter.add_issue(igniter, """
mob.adopt (LiveView mode) requires a stock `new LiveSocket(...)` in #{path}.
The current app.js shape is too customised for safe automated patching.
Either restore the standard Phoenix shape or use `--no-live-view`.
""")
true ->
igniter
end
end
defp check_root_html(igniter) do
web = "#{ProjectApplication.app_name(igniter)}_web"
candidates = [
"lib/#{web}/components/layouts/root.html.heex",
"lib/#{web}/templates/layout/root.html.heex"
]
case Enum.find(candidates, &Igniter.exists?(igniter, &1)) do
nil ->
Igniter.add_issue(igniter, """
mob.adopt (LiveView mode) requires a root layout at one of:
- lib/#{web}/components/layouts/root.html.heex
- lib/#{web}/templates/layout/root.html.heex
Neither was found. Use `--no-live-view` for thin-client mode.
""")
path ->
if has_body_tag?(igniter, path) do
igniter
else
Igniter.add_issue(igniter, """
mob.adopt (LiveView mode) requires a `<body>` tag in #{path} for the
bridge `<div>` injection. The current layout shape is too customised
for safe automated patching.
Either restore the standard layout or use `--no-live-view`.
""")
end
end
end
defp stock_live_socket?(igniter, path) do
case read_content(igniter, path) do
{:ok, content} -> String.contains?(content, "new LiveSocket(")
_ -> false
end
end
defp has_body_tag?(igniter, path) do
case read_content(igniter, path) do
{:ok, content} -> Regex.match?(Regex.compile!("<body[^>]*>"), content)
_ -> false
end
end
defp read_content(igniter, path) do
cond do
igniter.assigns[:test_mode?] ->
case igniter.assigns[:test_files][path] do
nil -> {:error, :not_found}
content -> {:ok, content}
end
File.regular?(path) ->
File.read(path)
true ->
{:error, :not_found}
end
end
end