MobDev.Adopt.Patcher (mob_dev v0.6.16)

Copy Markdown View Source

Pure helpers for the mix mob.adopt Elixir-source patches and content generators: the LiveView bridge patches (assets/js/app.js MobHook + root.html.heex bridge div), the mix.exs dep injection, and the generated mob_screen.ex / mob_app.ex / mob.exs / src/<app>.erl contents.

Duplicated from MobNew.LiveViewPatcher (mob_new, the project generator archive). Only the transitive closure mix mob.adopt actually exercises was copied — mob.new's notes-app generators (repo_content, note_content, the LiveView starter screens, …) stay in mob_new. Phase 5 of build_system_migration.md reunifies the two copies behind a single Igniter-based path; until then both repos carry their own copy (mob_new can't depend on mob_dev — it's a self-contained Mix archive; see ArchiveSelfContainedTest).

Compile-time regex

Like the mob_new original, this module compiles regexes at runtime via Regex.compile!/1 rather than ~r// literals — the ~r// form bakes a bytecode pattern that calls :re.import/1, removed in OTP 28.0 (the version Mob's bundled iOS/Android tarballs ship). See mob AGENTS.md rule #9.

Summary

Functions

Generates the Erlang bootstrap for a LiveView project.

Injects mob / mob_dev dependencies into the deps/0 function in mix.exs content.

Adds {:ecto_sqlite3, "~> 0.18"} to the deps list in mix.exs content if not already present. The generated mob_app.ex (LiveView flavour) calls Application.ensure_all_started(:ecto_sqlite3) and runs Ecto.Migrator on-device, so the dep is required whenever that template is emitted.

Injects the hidden bridge <div> immediately after the opening <body> tag.

Injects the MobHook definition and registration into the given app.js content.

Generates a thin-client <App>.MobApp for projects where the BEAM on device does NOT host Phoenix/Hologram/game state — instead the WebView points at a deployed Phoenix server and the device's BEAM is just the native interop layer.

Returns the hidden bridge element string (for test assertions).

Generates mob.exs config content for a LiveView project.

Returns the MobHook JS string (for tests and warning messages).

Generates the mob_app.ex entry point for a LiveView project.

Generates MobScreen content for mix mob.adopt.

Functions

erlang_entry_content(module_name, app_name)

@spec erlang_entry_content(String.t(), String.t()) :: String.t()

Generates the Erlang bootstrap for a LiveView project.

Calls ModuleName.MobApp.start() instead of ModuleName.App.start().

inject_deps(content, mob_dep, mob_dev_dep)

@spec inject_deps(String.t(), String.t(), String.t()) :: String.t()

Injects mob / mob_dev dependencies into the deps/0 function in mix.exs content.

mob_dep and mob_dev_dep are dependency tuple strings (already formatted — e.g. ~s({:mob, "~> 0.5"}) or ~s({:mob, path: "/path"})). They are parsed back to AST and inserted at the end of the user's deps list.

Idempotent: no-op if :mob is already declared in the user's deps list, regardless of indentation or trailing-comma shape.

Implementation note

Deps are injected by an AST walk (robust against Phoenix-version / formatter variation), then serialized with stdlib only (Macro.to_string + Code.format_string!). The mob_new original is reachable from mix mob.new running as a Mix archive, and archives don't bundle runtime deps — so it must not call any non-stdlib module (Sourceror). Preserved here verbatim.

inject_ecto_sqlite3(content)

@spec inject_ecto_sqlite3(String.t()) :: String.t()

Adds {:ecto_sqlite3, "~> 0.18"} to the deps list in mix.exs content if not already present. The generated mob_app.ex (LiveView flavour) calls Application.ensure_all_started(:ecto_sqlite3) and runs Ecto.Migrator on-device, so the dep is required whenever that template is emitted.

Idempotent — no-op when ecto_sqlite3 is already in the deps string.

inject_mob_bridge_element(content)

@spec inject_mob_bridge_element(String.t()) :: String.t()

Injects the hidden bridge <div> immediately after the opening <body> tag.

Idempotent: returns unchanged content if mob-bridge is already present.

inject_mob_hook(content)

@spec inject_mob_hook(String.t()) :: String.t()

Injects the MobHook definition and registration into the given app.js content.

Idempotent: returns unchanged content if MobHook is already present.

mob_app_content_thin(module_name, app_name)

@spec mob_app_content_thin(String.t(), String.t()) :: String.t()

Generates a thin-client <App>.MobApp for projects where the BEAM on device does NOT host Phoenix/Hologram/game state — instead the WebView points at a deployed Phoenix server and the device's BEAM is just the native interop layer.

Produced when mix mob.adopt --no-live-view is invoked. The thin variant uses use Mob.App with navigation/1 + on_start/0 callbacks (the same shape mix mob.new generates for native mode), rather than the LV-flavored def start do ... end that boots the host Phoenix endpoint on-device.

mob_bridge_element()

@spec mob_bridge_element() :: String.t()

Returns the hidden bridge element string (for test assertions).

mob_exs_content(mob_exs_mob_dir, mob_exs_elixir_lib)

@spec mob_exs_content(String.t(), String.t()) :: String.t()

Generates mob.exs config content for a LiveView project.

mob_hook_js()

@spec mob_hook_js() :: String.t()

Returns the MobHook JS string (for tests and warning messages).

mob_live_app_content(module_name, app_name, secret_key_base, signing_salt)

@spec mob_live_app_content(String.t(), String.t(), String.t(), String.t()) ::
  String.t()

Generates the mob_app.ex entry point for a LiveView project.

This module is called from the Erlang bootstrap (src/app_name.erl) instead of a native Mob.App module. It starts the Phoenix OTP application (which boots the endpoint) and then starts MobScreen to open the WebView.

Unlike native Mob apps, this does NOT use Mob.App — Phoenix owns the supervision tree. Mob is wired in at the BEAM entry level only.

secret_key_base and signing_salt are embedded directly because Mix config files (config/*.exs) are not loaded on-device — Application.put_env/3 is the only way to configure the endpoint before ensure_all_started/1 runs. The on-device port defaults to a per-app hash (4200..4999) — see default_liveview_port/0 for the collision rationale.

mob_screen_content_install(module_name)

@spec mob_screen_content_install(String.t()) :: String.t()

Generates MobScreen content for mix mob.adopt.

The generated module reads the WebView URL from application config:

config :mob, host_url: "https://your-app.example.com/"

Default if unset is http://127.0.0.1:4000/, suitable for on-device BEAM hitting a local Phoenix endpoint. mix mob.adopt --host-url <URL> writes the config entry so the user doesn't need to edit config/config.exs by hand.