Packages
mob_new
0.1.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
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.0
0.1.45
0.1.44
0.1.43
0.1.42
0.1.40
0.1.33
0.1.30
0.1.29
0.1.28
0.1.27
0.1.26
0.1.25
0.1.24
0.1.23
0.1.21
0.1.20
0.1.19
0.1.18
0.1.17
0.1.16
0.1.15
0.1.14
0.1.12
0.1.11
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Project generator for the Mob mobile framework
Current section
Files
Jump to
Current section
Files
lib/mix/tasks/mob.new.ex
defmodule Mix.Tasks.Mob.New do
use Mix.Task
@shortdoc "Create a new Mob mobile app project"
@moduledoc """
Creates a new Mob project with Android and iOS boilerplate.
mix mob.new APP_NAME [--liveview] [--no-install] [--no-ios] [--dest DIR] [--local]
## Options
* `--liveview` — generate a Phoenix LiveView app wrapped in a Mob WebView.
Calls `mix phx.new` to scaffold a Phoenix project, then
adds the Mob native boilerplate and LiveView bridge patches
(MobHook in app.js, mob-bridge element in root.html.heex,
MobScreen, mob.exs with liveview_port). Requires
`phx_new` archive to be installed (`mix archive.install hex phx_new`).
* `--no-install` — skip running `mix deps.get` after generation
* `--no-ios` — skip iOS boilerplate (use on Linux or Android-only projects)
* `--dest DIR` — create project in DIR (default: current directory)
* `--local` — use `path:` deps pointing to local mob/mob_dev repos
instead of hex version constraints. **For Mob framework
contributors only** — not intended for app developers.
Paths resolved from `MOB_DIR` / `MOB_DEV_DIR` env vars,
falling back to `./mob` / `./mob_dev`, then `../mob` /
`../mob_dev`. Also pre-fills `mob.exs` with real paths
so `mix mob.install` skips path configuration prompts.
## What gets generated (native mode, default)
APP_NAME/
mix.exs
lib/APP_NAME/app.ex
lib/APP_NAME/home_screen.ex
android/
settings.gradle
build.gradle
app/
build.gradle
src/main/
AndroidManifest.xml
java/com/mob/APP_NAME/MainActivity.kt
java/com/mob/APP_NAME/MobBridge.kt
java/com/mob/APP_NAME/MobNode.kt
java/com/mob/APP_NAME/MobScannerActivity.kt
gradle.properties
ios/
beam_main.m
Info.plist
## What gets generated (--liveview mode)
Everything `mix phx.new APP_NAME --no-install` generates, plus:
APP_NAME/
lib/APP_NAME/mob_screen.ex # Mob.Screen wrapping the Phoenix WebView
mob.exs # Mob config with liveview_port: 4000
android/ # same Android boilerplate as native mode
ios/ # same iOS boilerplate as native mode
Patches applied to the Phoenix project:
- `assets/js/app.js` — MobHook definition + registration
- `lib/APP_NAME_web/.../root.html.heex` — mob-bridge hidden div
- `lib/APP_NAME/application.ex` — Mob.App child in supervision tree
- `mix.exs` — mob / mob_dev deps added
After generation, run:
cd APP_NAME
mix mob.install # icon generation + any first-run setup
"""
@switches [
no_install: :boolean,
no_ios: :boolean,
dest: :string,
local: :boolean,
liveview: :boolean
]
@impl Mix.Task
def run(argv) do
{opts, args, _} = OptionParser.parse(argv, strict: @switches)
app_name = parse_app_name!(args)
{dest_dir, liveview, gen_opts} = parse_gen_opts(opts)
project_dir = Path.join(Path.expand(dest_dir), app_name)
log_flags(gen_opts, liveview)
Mix.shell().info([:green, "* creating ", :reset, project_dir])
case generate(app_name, dest_dir, liveview, gen_opts) do
{:error, reason} -> Mix.raise(reason)
{:ok, project_dir} -> post_generate(project_dir, app_name, liveview, gen_opts, opts)
end
end
defp parse_app_name!([name | _]), do: validate_app_name!(name)
defp parse_app_name!([]), do: Mix.raise("Usage: mix mob.new APP_NAME")
defp validate_app_name!(name) do
unless valid_app_name?(name) do
Mix.raise("App name must be lowercase letters, digits, and underscores only (e.g. my_app).")
end
name
end
defp parse_gen_opts(opts) do
dest_dir = opts[:dest] || "."
liveview = opts[:liveview] || false
gen_opts = [local: opts[:local] || false, no_ios: opts[:no_ios] || false]
{dest_dir, liveview, gen_opts}
end
defp log_flags(gen_opts, liveview) do
if gen_opts[:local] do
Mix.shell().info([:yellow, "* local mode: using path: deps for mob and mob_dev", :reset])
end
if gen_opts[:no_ios] do
Mix.shell().info([:yellow, "* --no-ios: skipping iOS boilerplate", :reset])
end
if liveview do
Mix.shell().info([
:cyan,
"* --liveview: generating Phoenix LiveView app with Mob bridge",
:reset
])
end
end
defp generate(app_name, dest_dir, true = _liveview, gen_opts) do
MobNew.ProjectGenerator.liveview_generate(app_name, dest_dir, gen_opts)
end
defp generate(app_name, dest_dir, false = _liveview, gen_opts) do
MobNew.ProjectGenerator.generate(app_name, dest_dir, gen_opts)
end
defp post_generate(project_dir, app_name, liveview, gen_opts, opts) do
unless liveview, do: print_created_files(project_dir, app_name, gen_opts[:no_ios])
unless opts[:no_install], do: fetch_deps(project_dir)
if liveview do
print_liveview_next_steps(app_name, opts[:no_install])
else
print_next_steps(app_name, opts[:no_install])
end
end
# ── private ──────────────────────────────────────────────────────────────────
defp fetch_deps(project_dir) do
Mix.shell().info("")
Mix.shell().info("Fetching dependencies...")
mix = System.find_executable("mix")
abs_dir = Path.expand(project_dir)
case System.cmd(mix, ["deps.get"], cd: abs_dir, into: IO.stream()) do
{_, 0} ->
:ok
{_, _} ->
Mix.shell().info([
:yellow,
"* deps.get failed — run it manually inside #{project_dir}",
:reset
])
end
end
defp print_created_files(project_dir, app_name, no_ios) do
files =
[
"mix.exs",
"lib/#{app_name}/app.ex",
"lib/#{app_name}/home_screen.ex",
"android/settings.gradle",
"android/build.gradle",
"android/app/build.gradle",
"android/app/src/main/AndroidManifest.xml",
"android/app/src/main/java/com/mob/#{app_name}/MainActivity.kt",
"android/app/src/main/java/com/mob/#{app_name}/MobBridge.kt",
"android/app/src/main/java/com/mob/#{app_name}/MobNode.kt",
"android/app/src/main/java/com/mob/#{app_name}/MobScannerActivity.kt",
"android/gradle.properties"
] ++ if(no_ios, do: [], else: ["ios/beam_main.m", "ios/Info.plist"])
Enum.each(files, fn f ->
Mix.shell().info([:green, "* creating ", :reset, Path.join(project_dir, f)])
end)
end
defp print_next_steps(app_name, no_install) do
install_hint =
if no_install,
do: "\n mix deps.get",
else: ""
Mix.shell().info("""
Your Mob app #{app_name} is ready!
#{install_hint}
cd #{app_name}
mix mob.install # generates app icon + first-run setup
First deploy — edit mob.exs and android/local.properties with your local
paths, then build native binaries (APK + iOS app), install on device,
and push BEAMs:
mix mob.deploy --native # first time, or after native code changes
Day-to-day development — just push changed BEAMs, no native rebuild needed:
mix mob.deploy # fast push + restart
mix mob.watch # auto-push on file save
""")
end
defp print_liveview_next_steps(app_name, no_install) do
install_hint =
if no_install,
do: "\n mix deps.get",
else: ""
Mix.shell().info("""
Your Mob LiveView app #{app_name} is ready!
#{install_hint}
Next steps:
1. Edit mob.exs with your local paths (mob_dir, elixir_lib).
2. Edit android/local.properties with your Android SDK path.
3. Run first-time setup:
cd #{app_name}
mix mob.install # icon generation + first-run setup
4. Configure your database in config/dev.exs and run:
mix ecto.create && mix ecto.migrate
5. Run the Phoenix server once to download JS/CSS dependencies and compile
assets. This is required before deploying — skipping it will result in
missing assets on device:
mix phx.server
Open http://localhost:4000 in your browser to confirm it loads, then
stop the server (Ctrl-C).
6. Deploy to device (first time — builds native APK/iOS app):
mix mob.deploy --native
The Mob WebView will load your Phoenix app at http://127.0.0.1:4000/.
Verify `window.mob.send` in browser devtools routes through `pushEvent`
(not `postMessage`) to confirm the LiveView bridge is active.
Day-to-day development:
mix mob.deploy # fast push + restart
mix mob.watch # auto-push on file save
""")
end
defp valid_app_name?(name), do: Regex.match?(~r/^[a-z][a-z0-9_]*$/, name)
end