# `MobDev.Enable`
[🔗](https://github.com/genericjam/mob_dev/blob/master/lib/mob_dev/enable.ex#L1)

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)`.

# `build_plist_entry`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

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

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

# `network_security_config_xml`

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

Returns the XML content for the Android network security config.

# `python_paths_module_template`

```elixir
@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`

```elixir
@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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
