MobDev.Enable (mob_dev v0.5.11)

Copy Markdown View Source

Pure helpers for mix mob.enable — extracted for testability.

LiveView bridge architecture

Enabling LiveView mode involves three coordinated patches. Understanding why all three are necessary prevents subtle bugs when setting up projects manually.

The two bridges

The native WebView (iOS WKWebView / Android WebView) injects a window.mob JavaScript object into every page it loads. This object routes calls through the NIF bridge:

window.mob.send(data)      // JS  NIF  Elixir handle_info
window.mob.onMessage(fn)   // registers handler for NIF  JS messages
window.mob._dispatch(json) // called by the NIF to deliver messages to JS

In LiveView mode you want a different routing: JS messages should travel over the LiveView WebSocket so that handle_event/3 in your LiveView receives them and push_event/3 delivers server messages to JS. The MobHook replaces window.mob with a LiveView-backed version on mount:

window.mob.send(data)      // JS  pushEvent("mob_message")  handle_event/3
window.mob.onMessage(fn)   // registers handler for handleEvent("mob_push")
window.mob._dispatch       // no-op: server messages arrive via handleEvent

Why a DOM element is required (the non-obvious part)

Phoenix LiveView hooks only execute their mounted() callback when an element carrying phx-hook="MobHook" is present in the rendered HTML and the LiveView WebSocket has connected. Registering MobHook in the hooks: map in app.js is necessary but not sufficient — the hook is dormant until LiveView finds a matching DOM element.

Without the element:

  • MobHook never mounts
  • window.mob is never replaced with the LiveView version
  • window.mob.send() routes through the native NIF bridge instead of LiveView
  • handle_event/3 never fires; your LiveView cannot receive JS messages

The element is a hidden <div> placed immediately after the opening <body> tag in root.html.heex:

<div id="mob-bridge" phx-hook="MobHook" style="display:none"></div>

Placing it at the top of <body> ensures the hook mounts as early as possible, so window.mob is overridden before any page-specific JS runs.

Android timing note

iOS injects the native window.mob shim via WKUserScript at .atDocumentStart — before any page JS runs. Android injects it via evaluateJavascript in onPageFinished — after the page has loaded. Between page load and onPageFinished on Android, window.mob is undefined. In practice LiveView connects after onPageFinished, so both shims are available by the time the MobHook mounts. If you call window.mob during DOMContentLoaded, guard with if (window.mob).

Summary

Functions

Builds a plist <key>/<value> entry for Info.plist injection.

Returns the canonical pyproject.toml string for a freshly-enabled Pythonx project. Used by the on_start template generator and by the desktop Pythonx.Uv.fetch/init calls in user code.

Inspects the project's existing native build templates for the markers mix mob.deploy --native expects when Pythonx is enabled. Returns a list of {relative_path, missing_marker} tuples for every file that exists but is missing the marker. An empty list means everything looks fresh.

Finds root.html.heex in a Phoenix project rooted at project_dir.

Adds android:networkSecurityConfig="@xml/network_security_config" to the <application> tag in an AndroidManifest.xml string.

Injects the hidden bridge <div> into content (a root.html.heex file).

Injects the MobHook definition and registration into content (the full text of assets/js/app.js).

Patches mix.exs content to add {:pythonx, "~> 0.4"} to the deps list when missing. Idempotent.

Returns the hidden bridge <div> element that must appear in root.html.heex.

Returns the MobHook JS constant to inject into app.js.

Returns the XML content for the Android network security config.

Returns the source for the <App>.PythonPaths module that mix mob.enable pythonx writes to lib/<app>/python_paths.ex.

Reads the app: atom from the given mix.exs path and returns the app name as a string, or raises.

Functions

build_plist_entry(key, value, opts \\ [])

@spec build_plist_entry(String.t(), term(), keyword()) :: String.t()

Builds a plist <key>/<value> entry for Info.plist injection.

Options:

  • type: :bool — emits <true/> or <false/> instead of <string>

default_pyproject_toml(app_name)

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

Returns the canonical pyproject.toml string for a freshly-enabled Pythonx project. Used by the on_start template generator and by the desktop Pythonx.Uv.fetch/init calls in user code.

detect_stale_pythonx_templates(project_dir, app_name)

@spec detect_stale_pythonx_templates(Path.t(), String.t()) :: [
  {String.t(), String.t()}
]

Inspects the project's existing native build templates for the markers mix mob.deploy --native expects when Pythonx is enabled. Returns a list of {relative_path, missing_marker} tuples for every file that exists but is missing the marker. An empty list means everything looks fresh.

We deliberately do not auto-patch — these files are typically hand-customized after mix mob.new, and silently inserting blocks is riskier than asking the user to copy from the template.

Files that don't exist yet (e.g. a project that never generated an ios/build.sh) are skipped — this is "stale-template detection," not "missing-platform detection."

find_root_html(project_dir, app_name)

@spec find_root_html(String.t(), String.t()) :: String.t() | nil

Finds root.html.heex in a Phoenix project rooted at project_dir.

Checks both the Phoenix 1.7+ convention:

lib/<app_name>_web/components/layouts/root.html.heex

and the pre-1.7 convention:

lib/<app_name>_web/templates/layout/root.html.heex

Returns the path string or nil if neither file exists.

inject_android_network_security_config(manifest_content)

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

Adds android:networkSecurityConfig="@xml/network_security_config" to the <application> tag in an AndroidManifest.xml string.

Idempotent — returns the content unchanged if the attribute is already present.

inject_mob_bridge_element(content)

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

Injects the hidden bridge <div> into content (a root.html.heex file).

The element is placed immediately after the opening <body> tag. This is the mount point for MobHook — without it the hook never executes and window.mob is never replaced with the LiveView version. See the module doc for the full explanation.

Returns the patched HTML string unchanged if id="mob-bridge" is already present.

inject_mob_hook(content)

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

Injects the MobHook definition and registration into content (the full text of assets/js/app.js).

  • Inserts the hook constant after the last top-level import line.
  • Registers MobHook in the hooks: option passed to LiveSocket.

Returns the patched JS string. Idempotency (skip if already present) is handled by the calling task, not by this function.

inject_pythonx_dep(content)

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

Patches mix.exs content to add {:pythonx, "~> 0.4"} to the deps list when missing. Idempotent.

Returns the (possibly-modified) content. Returns the original content unchanged when there's no recognizable defp deps do [ block — caller is expected to fall back to a friendly "couldn't find deps block" message.

mob_bridge_element()

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

Returns the hidden bridge <div> element that must appear in root.html.heex.

See the module doc for why this element is required.

mob_hook_js()

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

Returns the MobHook JS constant to inject into app.js.

network_security_config_xml()

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

Returns the XML content for the Android network security config.

python_paths_module_template(module_name)

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

Returns the source for the <App>.PythonPaths module that mix mob.enable pythonx writes to lib/<app>/python_paths.ex.

Pure — no filesystem access. The generated module supports iOS (paths under <otp_root>/python/) and Android (paths from MOB_PYTHON_HOME / MOB_PYTHON_DL env vars set by the user's MainActivity.kt before BEAM startup).

read_app_name_from(mix_exs_path)

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

Reads the app: atom from the given mix.exs path and returns the app name as a string, or raises.