defmodule Mix.Tasks.Lvn.Install do
@moduledoc "Installer Mix task for LiveView Native: `mix lvn.install`"
use Mix.Task
@requirements ["app.config"]
@template_projects_repo "https://github.com/liveview-native/liveview-native-template-projects"
@template_projects_version "0.0.1"
@shortdoc "Installs LiveView Native."
def run(args) do
{parsed_args, _, _} = OptionParser.parse(args, strict: [namespace: :string])
# Define some paths for the host project
current_path = File.cwd!()
mix_config_path = Path.join(current_path, "mix.exs")
app_config_path = Path.join(current_path, "/config/config.exs")
app_namespace = parsed_args[:namespace] || infer_app_namespace(mix_config_path)
build_path = Path.join(current_path, "_build")
libs_path = Path.join(build_path, "dev/lib")
# Ask the user some questions about their app
preferred_route_input =
IO.gets(
"What path should native clients connect to by default? Leave blank for default: \"/\")\n"
)
preferred_prod_url_input =
IO.gets("What URL will you use in production? Leave blank for default: \"example.com\")\n")
preferred_route = String.trim(preferred_route_input)
_preferred_route = if preferred_route == "", do: "/", else: preferred_route
preferred_prod_url = String.trim(preferred_prod_url_input)
_preferred_prod_url = if preferred_prod_url == "", do: "example.com", else: preferred_prod_url
# Get a list of compiled libraries
libs = File.ls!(libs_path)
# Clone the liveview-native-template-projects repo. This repo contains
# templates for various native platforms in their respective tools
# (Xcode, Android Studio, etc.)
clone_template_projects()
template_projects_path = Path.join(build_path, "lvn_tmp/liveview-native-template-projects")
template_libs = File.ls!(template_projects_path)
# Find libraries compiled for the host project that have available
# template projects
supported_libs = Enum.filter(libs, &(&1 in template_libs))
# Run the install script for each template project. Install scripts are
# responsible for generating platform-specific template projects and return
# information about that platform to be applied to the host project's Mix
# configuration.
platform_names =
Enum.map(supported_libs, fn lib ->
status_message("configuring", "#{lib}")
# Run the project-specific install script, passing info about the host
# Phoenix project.
lib_path = Path.join(template_projects_path, "/#{lib}")
script_path = Path.join(lib_path, "/install.exs")
cmd_opts = [
script_path,
"--app-name",
app_namespace,
"--app-path",
current_path,
"--platform-lib-path",
lib_path
]
with {platform_name, 0} <- System.cmd("elixir", cmd_opts) do
String.trim(platform_name)
end
end)
generate_native_exs_if_needed(current_path, platform_names)
update_config_exs_if_needed(app_config_path)
# Clear _build path to ensure it's rebuilt with new Config
File.rm_rf(build_path)
IO.puts("\nYour Phoenix app is ready to use LiveView Native!\n")
IO.puts("Platform-specific project files have been placed in the \"native\" directory\n")
:ok
end
###
defp clone_template_projects do
with {:ok, current_path} <- File.cwd(),
tmp_path <- Path.join(current_path, "_build/lvn_tmp"),
_ <- File.rm_rf(tmp_path),
:ok <- File.mkdir(tmp_path) do
status_message("downloading", "template project files")
System.cmd("git", [
"clone",
"-b",
@template_projects_version,
@template_projects_repo,
tmp_path <> "/liveview-native-template-projects"
])
end
end
def infer_app_namespace(config_path) do
with {:ok, config} <- File.read(config_path),
{:ok, mix_project_ast} <- Code.string_to_quoted(config),
{:ok, namespace} <- find_mix_project_namespace(mix_project_ast) do
"#{namespace}"
else
_ ->
raise "Could not infer Mix project namespace from mix.exs. Please provide it manually using the --namespace argument."
end
end
defp find_mix_project_namespace(ast) do
case ast do
ast when is_list(ast) ->
ast
|> Enum.reduce_while({:error, :cannot_infer_app_name}, fn node, _acc ->
{status, result} = find_mix_project_namespace(node)
acc_op = if status == :ok, do: :halt, else: :cont
{acc_op, {status, result}}
end)
{:defmodule, _, [{:__aliases__, _, [namespace, :MixProject]} | _rest]} ->
{:ok, namespace}
{:__block__, _, contents} ->
find_mix_project_namespace(contents)
_ ->
{:error, :cannot_infer_app_name}
end
end
defp generate_native_exs_if_needed(current_path, platform_names) do
platform_names_string = Enum.join(platform_names, ",")
native_config_path = Path.join(current_path, "/config/native.exs")
if File.exists?(native_config_path) do
IO.puts("native.exs already exists, skipping...")
else
status_message("creating", "config/native.exs")
# Generate native.exs and write it to config path
lvn_configuration = native_exs_body(platform_names_string)
{:ok, native_config} = File.open(native_config_path, [:write])
IO.binwrite(native_config, lvn_configuration)
File.close(native_config)
:ok
end
end
defp update_config_exs_if_needed(app_config_path) do
# Update project's config.exs to import native.exs if needed.
import_string = "import_config \"native.exs\""
{:ok, app_config_body} = File.read(app_config_path)
if String.contains?(app_config_body, import_string) do
IO.puts("config.exs already imports native.exs, skipping...")
else
status_message("updating", "config/config.exs")
{:ok, app_config} = File.open(app_config_path, [:write])
updated_app_config_body = app_config_body <> "\n" <> import_string
IO.binwrite(app_config, updated_app_config_body)
File.close(app_config)
end
end
defp native_exs_body(platform_names_string) do
"""
# This file is responsible for configuring LiveView Native.
# It is auto-generated when running `mix lvn.install`.
import Config
config :live_view_native, plugins: [#{platform_names_string}]
"""
end
defp status_message(label, message) do
formatted_message = IO.ANSI.green() <> "* #{label} " <> IO.ANSI.reset() <> message
IO.puts(formatted_message)
end
end