Skip to content

Commit

Permalink
Merge pull request #75 from liveview-native/improved-lvn-install
Browse files Browse the repository at this point in the history
Improve `lvn.install` task
  • Loading branch information
supernintendo authored Dec 8, 2023
2 parents 3fe5e43 + 2c9937e commit 79d465c
Show file tree
Hide file tree
Showing 3 changed files with 160 additions and 113 deletions.
268 changes: 156 additions & 112 deletions lib/mix/tasks/install.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,82 +4,20 @@ defmodule Mix.Tasks.Lvn.Install do

@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)
# Get all Mix tasks for LiveView Native client libraries
valid_mix_tasks = get_installer_mix_tasks()
host_project_config = get_host_project_config(parsed_args)

# Clear _build path to ensure it's rebuilt with new Config
File.rm_rf(build_path)
run_all_install_tasks(valid_mix_tasks, host_project_config)
native_config = merge_native_config(valid_mix_tasks)
generate_native_exs_if_needed(host_project_config, native_config)
update_config_exs_if_needed(host_project_config)
clean_build_path(host_project_config)
format_config_files()

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")
Expand All @@ -89,23 +27,111 @@ defmodule Mix.Tasks.Lvn.Install do

###

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"
])
defp run_all_install_tasks(mix_tasks, host_project_config) do
mix_tasks
|> Enum.map(&prompt_task_settings/1)
|> Enum.map(&(run_install_task(&1, host_project_config)))
end

defp prompt_task_settings(%{client_name: client_name, prompts: [_ | _] = prompts} = task) do
prompts
|> Enum.reduce_while({:ok, task}, fn {prompt_key, prompt_settings}, {:ok, acc} ->
case prompt_task_setting(prompt_settings, client_name) do
{:error, message} ->
Owl.IO.puts([Owl.Data.tag("#{client_name}: #{message}", :yellow)])

{:halt, {:error, acc}}

result ->
settings = Map.get(acc, :settings, %{})
updated_settings = Map.put(settings, prompt_key, result)

{:cont, {:ok, Map.put(acc, :settings, updated_settings)}}
end
end)
end

defp prompt_task_setting(%{ignore: true}, _client_name), do: true

defp prompt_task_setting(%{type: :confirm, label: label} = task, client_name) do
if Owl.IO.confirm(message: "#{client_name}: #{label}", default: true) do
if is_function(task[:on_yes]), do: apply(task[:on_yes], [])
else
if is_function(task[:on_no]), do: apply(task[:on_no], [])
end
end

defp prompt_task_setting(%{type: :multiselect, label: label, options: options, default: default} = task, client_name) do
default_label = Map.get(task, :default_label, inspect(default))

case Owl.IO.multiselect(options, label: "#{client_name}: #{label} (Space-delimited, leave blank for default: #{default_label})") do
[] ->
default || []

result ->
result
end
end

defp prompt_task_setting(_task, _client_name), do: nil

defp run_install_task(result, host_project_config) do
case result do
{:ok, %{client_name: client_name, mix_task: mix_task, settings: settings}} ->
Owl.IO.puts([Owl.Data.tag("* generating ", :green), "#{client_name} project files"])

mix_task.run(["--host-project-config", host_project_config, "--task-settings", settings])

_ ->
:skipped
end
end

defp get_installer_mix_tasks do
Mix.Task.load_all()
|> Enum.filter(&(function_exported?(&1, :lvn_install_config, 0)))
|> Enum.map(fn module ->
module
|> apply(:lvn_install_config, [])
|> Map.put(:mix_task, module)
end)
end

defp get_host_project_config(parsed_args) do
# Define some paths for the host project
current_path = File.cwd!()
mix_config_path = Path.join(current_path, "mix.exs")
build_path = Path.join(current_path, "_build")

# Ask the user some questiosn about the native project configuration
preferred_route = prompt_config_option("What path should native clients connect to by default?", "/")
preferred_prod_url = prompt_config_option("What URL will you use in production?", "example.com")
preferred_route = if String.starts_with?(preferred_route, "/"), do: preferred_route, else: "/#{preferred_route}"

%{
app_config_path: Path.join(current_path, "/config/config.exs"),
app_namespace: parsed_args[:namespace] || infer_app_namespace(mix_config_path),
build_path: build_path,
current_path: current_path,
libs_path: Path.join(build_path, "dev/lib"),
mix_config_path: mix_config_path,
native_path: Path.join(current_path, "native"),
preferred_prod_url: preferred_prod_url,
preferred_route: preferred_route
}
end

defp prompt_config_option(prompt_message, default_value) do
"#{prompt_message} (Leave blank for default: \"#{default_value}\")\n"
|> IO.gets()
|> String.trim()
|> default_if_blank(default_value)
end

defp default_if_blank(value, default_value) do
if value == "", do: default_value, else: value
end

def infer_app_namespace(config_path) do
with {:ok, config} <- File.read(config_path),
{:ok, mix_project_ast} <- Code.string_to_quoted(config),
Expand Down Expand Up @@ -139,56 +165,74 @@ defmodule Mix.Tasks.Lvn.Install do
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")
defp merge_native_config(mix_tasks) do
mix_tasks
|> Enum.reduce(%{}, fn %{mix_config: mix_config}, acc ->
DeepMerge.deep_merge(acc, mix_config)
end)
end

if File.exists?(native_config_path) do
IO.puts("native.exs already exists, skipping...")
else
status_message("creating", "config/native.exs")
defp generate_native_exs_if_needed(%{current_path: current_path}, %{} = native_config) do
native_config_path = Path.join(current_path, "/config/native.exs")
native_config_already_exists? = File.exists?(native_config_path)
generate_native_config? = if native_config_already_exists?, do: Owl.IO.confirm(message: "native.exs already exists, regenerate it?", default: false), else: true

# 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)
if generate_native_config? do
Owl.IO.puts([ Owl.Data.tag("* creating ", :green), "config/native.exs"])
lvn_configuration = native_exs_body(native_config)
File.write(native_config_path, lvn_configuration)

:ok
else
IO.puts("native.exs already exists, skipping...")
end
end

defp update_config_exs_if_needed(app_config_path) do
defp native_exs_body(%{} = native_config) do
config_body =
native_config
|> Enum.map(fn {key, config} ->
config_value = inspect(config)
config_value_formatted = String.slice(config_value, 1, String.length(config_value) - 2)

"config :#{key}, #{config_value_formatted}"
end)
|> Enum.join("\n\n")

"""
# This file is responsible for configuring LiveView Native.
# It is auto-generated when running `mix lvn.install`.
import Config
#{config_body}
"""
end

defp update_config_exs_if_needed(%{app_config_path: app_config_path}) do
# Update project's config.exs to import native.exs if needed.
import_string = "import_config \"native.exs\""
full_import_string = Enum.join(["\n", "# Import LiveView Native configuration", import_string], "\n")
{: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")
Owl.IO.puts([ Owl.Data.tag("* updating ", :yellow), "config/config.exs"])

{:ok, app_config} = File.open(app_config_path, [:write])
updated_app_config_body = app_config_body <> "\n" <> import_string
updated_app_config_body = app_config_body <> "\n" <> full_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}]
"""
defp format_config_files do
System.cmd("mix", ["format", "*.exs"], cd: "config")
end

defp status_message(label, message) do
formatted_message = IO.ANSI.green() <> "* #{label} " <> IO.ANSI.reset() <> message

IO.puts(formatted_message)
defp clean_build_path(%{build_path: build_path}) do
# Clear _build path to ensure it's rebuilt with new Config
File.rm_rf(build_path)
end
end
4 changes: 3 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ defmodule LiveViewNative.MixProject do
{:makeup_eex, ">= 0.1.1", only: :dev, runtime: false},
{:dialyxir, "~> 1.0", only: :dev, runtime: false},
{:meeseeks, "~> 0.17.0"},
{:live_view_native_platform, "0.2.0-beta.2"}
{:owl, "~> 0.8", runtime: false},
{:deep_merge, "~> 1.0"},
{:live_view_native_platform, "0.2.0-beta.2"},
]
end

Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"meeseeks_html5ever": {:hex, :meeseeks_html5ever, "0.14.3", "7827c6ce393d9f99dd0220c356fd66ee7101718037ec6f7f18d4bcba84ef1798", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6.1", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "6b69573b97120fcc6e97045178ad085fd3ee10a5b49c1e9ebb8a28bd4a9c538b"},
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"owl": {:hex, :owl, "0.8.0", "0ef925cb784311093d4e3734822960cbdbdb13b095d748bb5bc82abcd5b56732", [:mix], [], "hexpm", "0a5586ceb1a12f4bbda90e330c20e6ea034552335d09466c10e4218c98977529"},
"phoenix": {:hex, :phoenix, "1.7.7", "4cc501d4d823015007ba3cdd9c41ecaaf2ffb619d6fb283199fa8ddba89191e0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "8966e15c395e5e37591b6ed0bd2ae7f48e961f0f60ac4c733f9566b519453085"},
"phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.20.0", "3f3531c835e46a3b45b4c3ca4a09cef7ba1d0f0d0035eef751c7084b8adb1299", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "29875f8a58fb031f2dc8f3be025c92ed78d342b46f9bbf6dfe579549d7c81050"},
Expand Down

0 comments on commit 79d465c

Please sign in to comment.