From 9a043b6bf040453b539569cc9383a72e0fe53b92 Mon Sep 17 00:00:00 2001 From: Leandro Pereira Date: Sat, 2 Nov 2024 18:56:40 -0400 Subject: [PATCH 1/5] wip: install_phoenix task --- lib/igniter.ex | 83 +++++++------ lib/igniter/phoenix/generator.ex | 117 ++++++++++++++++++ lib/igniter/phoenix/single.ex | 71 +++++++++++ lib/igniter/project/config.ex | 7 +- lib/mix/tasks/igniter.install_phoenix.ex | 52 ++++++++ mix.exs | 1 + mix.lock | 1 + .../tasks/igniter.install_phoenix_test.exs | 10 ++ 8 files changed, 301 insertions(+), 41 deletions(-) create mode 100644 lib/igniter/phoenix/generator.ex create mode 100644 lib/igniter/phoenix/single.ex create mode 100644 lib/mix/tasks/igniter.install_phoenix.ex create mode 100644 test/mix/tasks/igniter.install_phoenix_test.exs diff --git a/lib/igniter.ex b/lib/igniter.ex index 9506e09..adef8ea 100644 --- a/lib/igniter.ex +++ b/lib/igniter.ex @@ -1063,48 +1063,54 @@ defmodule Igniter do source -> source end - if Rewrite.Source.from?(source, :string) do - content_lines = - source - |> Rewrite.Source.get(:content) - |> String.split("\n") - |> Enum.with_index() - - space_padding = - content_lines - |> Enum.map(&elem(&1, 1)) - |> Enum.max() - |> to_string() - |> String.length() - - diffish_looking_text = - Enum.map_join(content_lines, "\n", fn {line, line_number_minus_one} -> - line_number = line_number_minus_one + 1 - - "#{String.pad_trailing(to_string(line_number), space_padding)} #{color(IO.ANSI.yellow(), color?)}| #{color(IO.ANSI.green(), color?)}#{line}#{color(IO.ANSI.reset(), color?)}" - end) + cond do + source.filetype && Rewrite.Source.from?(source, :string) -> + content_lines = + source + |> Rewrite.Source.get(:content) + |> String.split("\n") + |> Enum.with_index() + + space_padding = + content_lines + |> Enum.map(&elem(&1, 1)) + |> Enum.max() + |> to_string() + |> String.length() + + diffish_looking_text = + Enum.map_join(content_lines, "\n", fn {line, line_number_minus_one} -> + line_number = line_number_minus_one + 1 + + "#{String.pad_trailing(to_string(line_number), space_padding)} #{color(IO.ANSI.yellow(), color?)}| #{color(IO.ANSI.green(), color?)}#{line}#{color(IO.ANSI.reset(), color?)}" + end) - if String.trim(diffish_looking_text) != "" do - """ - Create: #{Rewrite.Source.get(source, :path)} + if String.trim(diffish_looking_text) != "" do + """ + Create: #{Rewrite.Source.get(source, :path)} - #{diffish_looking_text} - """ - else - "" - end - else - diff = Rewrite.Source.diff(source, color: color?) |> IO.iodata_to_binary() + #{diffish_looking_text} + """ + else + "" + end - if String.trim(diff) != "" do - """ - Update: #{Rewrite.Source.get(source, :path)} + source.filetype -> + diff = Rewrite.Source.diff(source, color: color?) |> IO.iodata_to_binary() - #{diff} - """ - else + if String.trim(diff) != "" do + """ + Update: #{Rewrite.Source.get(source, :path)} + + #{diff} + """ + else + "" + end + + # FIXME: binary files + :else -> "" - end end end) end @@ -1177,7 +1183,8 @@ defmodule Igniter do Rewrite.map!(rewrite, fn source -> path = source |> Rewrite.Source.get(:path) - if is_nil(adding_paths) || path in List.wrap(adding_paths) do + if source_handler(path) == Rewrite.Source.Ex && + (is_nil(adding_paths) || path in List.wrap(adding_paths)) do dir = Path.dirname(path) opts = diff --git a/lib/igniter/phoenix/generator.ex b/lib/igniter/phoenix/generator.ex new file mode 100644 index 0000000..038f0be --- /dev/null +++ b/lib/igniter/phoenix/generator.ex @@ -0,0 +1,117 @@ +defmodule Igniter.Phoenix.Generator do + @moduledoc false + # Wrap Phx.New.Generator + # https://github.com/phoenixframework/phoenix/blob/7586cbee9e37afbe0b3cdbd560b9e6aa60d32bf6/installer/lib/phx_new/generator.ex#L69 + + def copy_from(igniter, project, mod, name) when is_atom(name) do + mapping = mod.template_files(name) + + templates = + for {format, _project_location, files} <- mapping, + {source, target_path} <- files, + source = to_string(source) do + # target = Phx.New.Project.join_path(project, project_location, target_path) + target = expand_path_with_bindings(target_path, project) + {format, source, target} + end + + Enum.reduce(templates, igniter, fn {format, source, target}, acc -> + case format do + :keep -> + acc + + :text -> + contents = mod.render(name, source, project.binding) + Igniter.create_new_file(acc, target, contents, on_exists: :overwrite) + + :config -> + contents = mod.render(name, source, project.binding) + config_inject(acc, target, contents) + + :prod_config -> + contents = mod.render(name, source, project.binding) + prod_only_config_inject(acc, target, contents) + + :eex -> + contents = mod.render(name, source, project.binding) + Igniter.create_new_file(acc, target, contents, on_exists: :overwrite) + end + end) + end + + defp expand_path_with_bindings(path, project) do + Regex.replace(Regex.recompile!(~r/:[a-zA-Z0-9_]+/), path, fn ":" <> key, _ -> + project |> Map.fetch!(:"#{key}") |> to_string() + end) + end + + defp config_inject(igniter, file, to_inject) do + patterns = [ + """ + import Config + __cursor__() + """ + ] + + Igniter.create_or_update_elixir_file(igniter, file, to_inject, fn zipper -> + case Igniter.Code.Common.move_to_cursor_match_in_scope(zipper, patterns) do + {:ok, zipper} -> + {:ok, Igniter.Code.Common.add_code(zipper, to_inject)} + + _ -> + {:warning, + """ + Could not automatically inject the following config into #{file} + + #{to_inject} + """} + end + end) + end + + defp prod_only_config_inject(igniter, file, to_inject) do + patterns = [ + """ + if config_env() == :prod do + __cursor__() + end + """, + """ + if :prod == config_env() do + __cursor__() + end + """ + ] + + Igniter.create_or_update_elixir_file(igniter, file, to_inject, fn zipper -> + case Igniter.Code.Common.move_to_cursor_match_in_scope(zipper, patterns) do + {:ok, zipper} -> + {:ok, Igniter.Code.Common.add_code(zipper, to_inject)} + + _ -> + {:warning, + """ + Could not automatically inject the following config into #{file} + + #{to_inject} + """} + end + end) + end + + def gen_ecto_config(igniter, %{binding: binding}) do + adapter_config = binding[:adapter_config] + + config_inject(igniter, "config/dev.exs", """ + # Configure your database + config :#{binding[:app_name]}, #{binding[:app_module]}.Repo#{kw_to_config(adapter_config[:dev])} + """) + end + + defp kw_to_config(kw) do + Enum.map(kw, fn + {k, {:literal, v}} -> ",\n #{k}: #{v}" + {k, v} -> ",\n #{k}: #{inspect(v)}" + end) + end +end diff --git a/lib/igniter/phoenix/single.ex b/lib/igniter/phoenix/single.ex new file mode 100644 index 0000000..9b91356 --- /dev/null +++ b/lib/igniter/phoenix/single.ex @@ -0,0 +1,71 @@ +defmodule Igniter.Phoenix.Single do + @moduledoc false + # Wrap Phx.New.Single + # https://github.com/phoenixframework/phoenix/blob/7586cbee9e37afbe0b3cdbd560b9e6aa60d32bf6/installer/lib/phx_new/single.ex + + alias Phx.New.Project + alias Igniter.Phoenix.Generator + + @mod Phx.New.Single + + def generate(igniter, project) do + generators = [ + {true, &gen_new/2}, + {Project.ecto?(project), &gen_ecto/2}, + {Project.html?(project), &gen_html/2}, + {Project.mailer?(project), &gen_mailer/2}, + {Project.gettext?(project), &gen_gettext/2}, + {true, &gen_assets/2} + ] + + Enum.reduce(generators, igniter, fn + {true, gen_fun}, acc -> gen_fun.(acc, project) + _, acc -> acc + end) + end + + def gen_new(igniter, project) do + Generator.copy_from(igniter, project, @mod, :new) + end + + def gen_ecto(igniter, project) do + igniter + |> Generator.copy_from(project, @mod, :ecto) + |> Generator.gen_ecto_config(project) + end + + def gen_html(igniter, project) do + Generator.copy_from(igniter, project, @mod, :html) + end + + def gen_mailer(igniter, project) do + Generator.copy_from(igniter, project, @mod, :mailer) + end + + def gen_gettext(igniter, project) do + Generator.copy_from(igniter, project, @mod, :gettext) + end + + def gen_assets(igniter, project) do + javascript? = Project.javascript?(project) + css? = Project.css?(project) + html? = Project.html?(project) + + igniter = Generator.copy_from(igniter, project, @mod, :static) + + igniter = + if html? or javascript? do + command = if javascript?, do: :js, else: :no_js + Generator.copy_from(igniter, project, @mod, command) + else + igniter + end + + if html? or css? do + command = if css?, do: :css, else: :no_css + Generator.copy_from(igniter, project, @mod, command) + else + igniter + end + end +end diff --git a/lib/igniter/project/config.ex b/lib/igniter/project/config.ex index e0a072a..0cf59a0 100644 --- a/lib/igniter/project/config.ex +++ b/lib/igniter/project/config.ex @@ -189,8 +189,9 @@ defmodule Igniter.Project.Config do end) end - defp ensure_default_configs_exist(igniter, file) - when file in ["config/dev.exs", "config/test.exs", "config/prod.exs"] do + @doc false + def ensure_default_configs_exist(igniter, file) + when file in ["config/dev.exs", "config/test.exs", "config/prod.exs"] do igniter |> Igniter.include_or_create_file("config/config.exs", """ import Config @@ -207,7 +208,7 @@ defmodule Igniter.Project.Config do """) end - defp ensure_default_configs_exist(igniter, _), do: igniter + def ensure_default_configs_exist(igniter, _), do: igniter defp ensure_config_evaluates_env(igniter) do Igniter.update_elixir_file(igniter, "config/config.exs", fn zipper -> diff --git a/lib/mix/tasks/igniter.install_phoenix.ex b/lib/mix/tasks/igniter.install_phoenix.ex new file mode 100644 index 0000000..9d2c83a --- /dev/null +++ b/lib/mix/tasks/igniter.install_phoenix.ex @@ -0,0 +1,52 @@ +defmodule Mix.Tasks.Igniter.InstallPhoenix do + use Igniter.Mix.Task + + @example "mix igniter.install_phoenix" + @shortdoc "Install Phoenix project files" + + @moduledoc """ + #{@shortdoc} + + ## Example + + ```bash + #{@example} + ``` + + ## Options + + # TODO: phx.new options (--umbrella, --no-ecto, etc) + # https://github.com/phoenixframework/phoenix/blob/7586cbee9e37afbe0b3cdbd560b9e6aa60d32bf6/installer/lib/mix/tasks/phx.new.ex#L13 + """ + + def info(_argv, _source) do + %Igniter.Mix.Task.Info{ + group: :igniter, + example: @example, + positional: [:base_path] + } + end + + def igniter(igniter, argv) do + # TODO: check elixir version - https://github.com/phoenixframework/phoenix/blob/7586cbee9e37afbe0b3cdbd560b9e6aa60d32bf6/installer/lib/mix/tasks/phx.new.ex#L380 + + {%{base_path: base_path}, argv} = positional_args!(argv) + _options = options!(argv) + + # TODO: umbrella + generate(igniter, base_path, {Phx.New.Single, Igniter.Phoenix.Single}, :base_path) + end + + # TODO: opts + # TODO: call validate_project(path) + # TODO: perform some of the validations - https://github.com/phoenixframework/phoenix/blob/7586cbee9e37afbe0b3cdbd560b9e6aa60d32bf6/installer/lib/mix/tasks/phx.new.ex#L187 + defp generate(igniter, base_path, {phx_generator, igniter_generator}, _path, opts \\ []) do + project = + base_path + |> Phx.New.Project.new(opts) + |> phx_generator.prepare_project() + |> Phx.New.Generator.put_binding() + + igniter_generator.generate(igniter, project) + end +end diff --git a/mix.exs b/mix.exs index d611360..d036a2e 100644 --- a/mix.exs +++ b/mix.exs @@ -92,6 +92,7 @@ defmodule Igniter.MixProject do {:spitfire, "~> 0.1 and >= 0.1.3"}, {:sourceror, "~> 1.4"}, {:jason, "~> 1.4"}, + {:phx_new, "~> 1.7", runtime: false}, # Dev/Test dependencies {:eflame, "~> 1.0", only: [:dev, :test]}, {:ex_doc, "~> 0.32", only: [:dev, :test], runtime: false}, diff --git a/mix.lock b/mix.lock index 82c840e..b7e4404 100644 --- a/mix.lock +++ b/mix.lock @@ -24,6 +24,7 @@ "mix_audit": {:hex, :mix_audit, "2.1.4", "0a23d5b07350cdd69001c13882a4f5fb9f90fbd4cbf2ebc190a2ee0d187ea3e9", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "fd807653cc8c1cada2911129c7eb9e985e3cc76ebf26f4dd628bb25bbcaa7099"}, "mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "phx_new": {:hex, :phx_new, "1.7.14", "30d2d38b78bb762452595fe2e32f3a4a838f26e87713024840059884204ff141", [:mix], [], "hexpm", "e1a8b3839a9a2d94bceb95d96ca1f175264c751405fee8f39a4e67b379314a39"}, "rewrite": {:hex, :rewrite, "0.10.5", "6afadeae0b9d843b27ac6225e88e165884875e0aed333ef4ad3bf36f9c101bed", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "51cc347a4269ad3a1e7a2c4122dbac9198302b082f5615964358b4635ebf3d4f"}, "sourceror": {:hex, :sourceror, "1.6.0", "9907884e1449a4bd7dbaabe95088ed4d9a09c3c791fb0103964e6316bc9448a7", [:mix], [], "hexpm", "e90aef8c82dacf32c89c8ef83d1416fc343cd3e5556773eeffd2c1e3f991f699"}, "spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"}, diff --git a/test/mix/tasks/igniter.install_phoenix_test.exs b/test/mix/tasks/igniter.install_phoenix_test.exs new file mode 100644 index 0000000..7968d91 --- /dev/null +++ b/test/mix/tasks/igniter.install_phoenix_test.exs @@ -0,0 +1,10 @@ +defmodule Mix.Tasks.Igniter.InstallPhoenixTest do + use ExUnit.Case + import Igniter.Test + + test "config_inject" do + test_project() + |> Igniter.compose_task("igniter.install_phoenix", ["my_app"]) + |> assert_creates("config/dev.exs") + end +end From c430ecf8a36a6d83b016b8509d6bd83221bbceda Mon Sep 17 00:00:00 2001 From: Leandro Pereira Date: Sat, 2 Nov 2024 19:02:44 -0400 Subject: [PATCH 2/5] phx.new --- lib/igniter.ex | 17 +++++++++++++---- lib/igniter/project/config.ex | 4 ++-- lib/mix/tasks/igniter.install_phoenix.ex | 5 ++--- mix.lock | 1 + test/mix/tasks/igniter.install_phoenix_test.exs | 2 +- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/igniter.ex b/lib/igniter.ex index 70454fd..5442525 100644 --- a/lib/igniter.ex +++ b/lib/igniter.ex @@ -1124,7 +1124,8 @@ defmodule Igniter do end cond do - source.filetype && Rewrite.Source.from?(source, :string) -> + Rewrite.Source.from?(source, :string) && + String.valid?(Rewrite.Source.get(source, :content)) -> content_lines = source |> Rewrite.Source.get(:content) @@ -1155,7 +1156,7 @@ defmodule Igniter do "" end - source.filetype -> + String.valid?(Rewrite.Source.get(source, :content)) -> diff = Rewrite.Source.diff(source, color: color?) |> IO.iodata_to_binary() if String.trim(diff) != "" do @@ -1168,8 +1169,15 @@ defmodule Igniter do "" end - # FIXME: binary files + !String.valid?(Rewrite.Source.get(source, :content)) -> + """ + Create: #{Rewrite.Source.get(source, :path)} + + (content diff can't be displayed) + """ + :else -> + dbg(source) "" end end) @@ -1423,7 +1431,8 @@ defmodule Igniter do warnings: Enum.uniq(igniter.warnings), tasks: Enum.uniq(igniter.tasks) } - |> Igniter.Project.Module.move_files() + # FIXME + # |> Igniter.Project.Module.move_files() |> remove_unchanged_files() |> then(fn igniter -> if needs_test_support? do diff --git a/lib/igniter/project/config.ex b/lib/igniter/project/config.ex index 0cf59a0..c5e9229 100644 --- a/lib/igniter/project/config.ex +++ b/lib/igniter/project/config.ex @@ -190,7 +190,7 @@ defmodule Igniter.Project.Config do end @doc false - def ensure_default_configs_exist(igniter, file) + defp ensure_default_configs_exist(igniter, file) when file in ["config/dev.exs", "config/test.exs", "config/prod.exs"] do igniter |> Igniter.include_or_create_file("config/config.exs", """ @@ -208,7 +208,7 @@ defmodule Igniter.Project.Config do """) end - def ensure_default_configs_exist(igniter, _), do: igniter + defp ensure_default_configs_exist(igniter, _), do: igniter defp ensure_config_evaluates_env(igniter) do Igniter.update_elixir_file(igniter, "config/config.exs", fn zipper -> diff --git a/lib/mix/tasks/igniter.install_phoenix.ex b/lib/mix/tasks/igniter.install_phoenix.ex index 9d2c83a..b3b20f4 100644 --- a/lib/mix/tasks/igniter.install_phoenix.ex +++ b/lib/mix/tasks/igniter.install_phoenix.ex @@ -27,11 +27,10 @@ defmodule Mix.Tasks.Igniter.InstallPhoenix do } end - def igniter(igniter, argv) do + def igniter(igniter) do # TODO: check elixir version - https://github.com/phoenixframework/phoenix/blob/7586cbee9e37afbe0b3cdbd560b9e6aa60d32bf6/installer/lib/mix/tasks/phx.new.ex#L380 - {%{base_path: base_path}, argv} = positional_args!(argv) - _options = options!(argv) + %{base_path: base_path} = igniter.args.positional # TODO: umbrella generate(igniter, base_path, {Phx.New.Single, Igniter.Phoenix.Single}, :base_path) diff --git a/mix.lock b/mix.lock index a9e5b20..bc89cd8 100644 --- a/mix.lock +++ b/mix.lock @@ -24,6 +24,7 @@ "mix_audit": {:hex, :mix_audit, "2.1.4", "0a23d5b07350cdd69001c13882a4f5fb9f90fbd4cbf2ebc190a2ee0d187ea3e9", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "fd807653cc8c1cada2911129c7eb9e985e3cc76ebf26f4dd628bb25bbcaa7099"}, "mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "phx_new": {:hex, :phx_new, "1.7.14", "30d2d38b78bb762452595fe2e32f3a4a838f26e87713024840059884204ff141", [:mix], [], "hexpm", "e1a8b3839a9a2d94bceb95d96ca1f175264c751405fee8f39a4e67b379314a39"}, "rewrite": {:hex, :rewrite, "1.0.0", "fddda21eb62c2b9dbd6cf35b39984d02761a63c25de2947ceab7ffe2729fa2e5", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "701975d9d0c21b1f40a82881e489aca6b53893f095f318978c6d2899299a514b"}, "sourceror": {:hex, :sourceror, "1.7.0", "62c34f4e3a109d837edd652730219b6332745e0ec7db34d5d350a5cdf30b376a", [:mix], [], "hexpm", "3dd2b1bd780fd0df48089f48480a54fd065bf815b63ef8046219d7784e7435f3"}, "spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"}, diff --git a/test/mix/tasks/igniter.install_phoenix_test.exs b/test/mix/tasks/igniter.install_phoenix_test.exs index 7968d91..1e65f57 100644 --- a/test/mix/tasks/igniter.install_phoenix_test.exs +++ b/test/mix/tasks/igniter.install_phoenix_test.exs @@ -5,6 +5,6 @@ defmodule Mix.Tasks.Igniter.InstallPhoenixTest do test "config_inject" do test_project() |> Igniter.compose_task("igniter.install_phoenix", ["my_app"]) - |> assert_creates("config/dev.exs") + |> assert_creates("lib/my_app_web/components/core_components.ex") end end From 7ec291ee48f141f9813b1c67e4b5b560103c7a4b Mon Sep 17 00:00:00 2001 From: Leandro Pereira Date: Sun, 3 Nov 2024 09:27:56 -0500 Subject: [PATCH 3/5] mix format --- lib/igniter/project/config.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/igniter/project/config.ex b/lib/igniter/project/config.ex index c5e9229..57d88bc 100644 --- a/lib/igniter/project/config.ex +++ b/lib/igniter/project/config.ex @@ -191,7 +191,7 @@ defmodule Igniter.Project.Config do @doc false defp ensure_default_configs_exist(igniter, file) - when file in ["config/dev.exs", "config/test.exs", "config/prod.exs"] do + when file in ["config/dev.exs", "config/test.exs", "config/prod.exs"] do igniter |> Igniter.include_or_create_file("config/config.exs", """ import Config From 831845ede529cb2b130751fb7087d7b85032fb28 Mon Sep 17 00:00:00 2001 From: Leandro Pereira Date: Wed, 6 Nov 2024 16:57:36 -0500 Subject: [PATCH 4/5] Adress feedback: - make :phx_new optional and check if it's loaded - add remaining user-facing opts - require Elixir ~> 1.15 - validate %Project{} - use Phoenix extension --- lib/igniter.ex | 3 +- lib/igniter/phoenix/generator.ex | 1 - lib/mix/tasks/igniter.install_phoenix.ex | 156 ++++++++++++++++-- mix.exs | 2 +- mix.lock | 1 + .../tasks/igniter.install_phoenix_test.exs | 19 ++- 6 files changed, 163 insertions(+), 19 deletions(-) diff --git a/lib/igniter.ex b/lib/igniter.ex index 8332e98..0681a68 100644 --- a/lib/igniter.ex +++ b/lib/igniter.ex @@ -1438,8 +1438,7 @@ defmodule Igniter do warnings: Enum.uniq(igniter.warnings), tasks: Enum.uniq(igniter.tasks) } - # FIXME - # |> Igniter.Project.Module.move_files() + |> Igniter.Project.Module.move_files() |> remove_unchanged_files() |> then(fn igniter -> if needs_test_support? do diff --git a/lib/igniter/phoenix/generator.ex b/lib/igniter/phoenix/generator.ex index 038f0be..644c18b 100644 --- a/lib/igniter/phoenix/generator.ex +++ b/lib/igniter/phoenix/generator.ex @@ -10,7 +10,6 @@ defmodule Igniter.Phoenix.Generator do for {format, _project_location, files} <- mapping, {source, target_path} <- files, source = to_string(source) do - # target = Phx.New.Project.join_path(project, project_location, target_path) target = expand_path_with_bindings(target_path, project) {format, source, target} end diff --git a/lib/mix/tasks/igniter.install_phoenix.ex b/lib/mix/tasks/igniter.install_phoenix.ex index b3b20f4..9a4d1db 100644 --- a/lib/mix/tasks/igniter.install_phoenix.ex +++ b/lib/mix/tasks/igniter.install_phoenix.ex @@ -1,8 +1,8 @@ defmodule Mix.Tasks.Igniter.InstallPhoenix do use Igniter.Mix.Task - @example "mix igniter.install_phoenix" - @shortdoc "Install Phoenix project files" + @example "mix igniter.install_phoenix . --module MyApp --app my_app" + @shortdoc "Creates a new Phoenix project in the current application." @moduledoc """ #{@shortdoc} @@ -15,37 +15,167 @@ defmodule Mix.Tasks.Igniter.InstallPhoenix do ## Options - # TODO: phx.new options (--umbrella, --no-ecto, etc) - # https://github.com/phoenixframework/phoenix/blob/7586cbee9e37afbe0b3cdbd560b9e6aa60d32bf6/installer/lib/mix/tasks/phx.new.ex#L13 + * `--app` - the name of the OTP application + + * `--module` - the name of the base module in + the generated skeleton + + * `--database` - specify the database adapter for Ecto. One of: + + * `postgres` - via https://github.com/elixir-ecto/postgrex + * `mysql` - via https://github.com/elixir-ecto/myxql + * `mssql` - via https://github.com/livehelpnow/tds + * `sqlite3` - via https://github.com/elixir-sqlite/ecto_sqlite3 + + Please check the driver docs for more information + and requirements. Defaults to "postgres". + + * `--adapter` - specify the http adapter. One of: + * `cowboy` - via https://github.com/elixir-plug/plug_cowboy + * `bandit` - via https://github.com/mtrudel/bandit + + Please check the adapter docs for more information + and requirements. Defaults to "bandit". + + * `--no-assets` - equivalent to `--no-esbuild` and `--no-tailwind` + + * `--no-dashboard` - do not include Phoenix.LiveDashboard + + * `--no-ecto` - do not generate Ecto files + + * `--no-esbuild` - do not include esbuild dependencies and assets. + We do not recommend setting this option, unless for API only + applications, as doing so requires you to manually add and + track JavaScript dependencies + + * `--no-gettext` - do not generate gettext files + + * `--no-html` - do not generate HTML views + + * `--no-live` - comment out LiveView socket setup in your Endpoint + and assets/js/app.js. Automatically disabled if --no-html is given + + * `--no-mailer` - do not generate Swoosh mailer files + + * `--no-tailwind` - do not include tailwind dependencies and assets. + The generated markup will still include Tailwind CSS classes, those + are left-in as reference for the subsequent styling of your layout + and components + + * `--binary-id` - use `binary_id` as primary key type in Ecto schemas + + * `--verbose` - use verbose output + + When passing the `--no-ecto` flag, Phoenix generators such as + `phx.gen.html`, `phx.gen.json`, `phx.gen.live`, and `phx.gen.context` + may no longer work as expected as they generate context files that rely + on Ecto for the database access. In those cases, you can pass the + `--no-context` flag to generate most of the HTML and JSON files + but skip the context, allowing you to fill in the blanks as desired. + + Similarly, if `--no-html` is given, the files generated by + `phx.gen.html` will no longer work, as important HTML components + will be missing. + """ def info(_argv, _source) do %Igniter.Mix.Task.Info{ group: :igniter, example: @example, - positional: [:base_path] + positional: [:base_path], + schema: [ + app: :string, + module: :string, + database: :string, + adapter: :string, + assets: :boolean, + dashboard: :boolean, + ecto: :boolean, + esbuild: :boolean, + gettext: :boolean, + html: :boolean, + live: :boolean, + mailer: :boolean, + tailwind: :boolean, + binary_id: :boolean, + verbose: :boolean + ] } end def igniter(igniter) do - # TODO: check elixir version - https://github.com/phoenixframework/phoenix/blob/7586cbee9e37afbe0b3cdbd560b9e6aa60d32bf6/installer/lib/mix/tasks/phx.new.ex#L380 + elixir_version_check!() + + if !Code.ensure_loaded?(Phx.New.Generator) do + Mix.raise(""" + Phoenix installer is not available. Please install it before proceding: + + mix archive.install hex phx_new + + """) + end + + if igniter.args.options[:umbrella] do + Mix.raise("Umbrella projects are not supported yet.") + end %{base_path: base_path} = igniter.args.positional - # TODO: umbrella - generate(igniter, base_path, {Phx.New.Single, Igniter.Phoenix.Single}, :base_path) + generate(igniter, base_path, {Phx.New.Single, Igniter.Phoenix.Single}, igniter.args.options) end - # TODO: opts - # TODO: call validate_project(path) - # TODO: perform some of the validations - https://github.com/phoenixframework/phoenix/blob/7586cbee9e37afbe0b3cdbd560b9e6aa60d32bf6/installer/lib/mix/tasks/phx.new.ex#L187 - defp generate(igniter, base_path, {phx_generator, igniter_generator}, _path, opts \\ []) do + defp generate(igniter, base_path, {phx_generator, igniter_generator}, opts) do project = base_path |> Phx.New.Project.new(opts) |> phx_generator.prepare_project() |> Phx.New.Generator.put_binding() + |> validate_project() + + igniter + |> Igniter.compose_task("igniter.add_extension", ["phoenix"]) + |> igniter_generator.generate(project) + end + + defp validate_project(%Phx.New.Project{opts: opts} = project) do + check_app_name!(project.app, !!opts[:app]) + check_module_name_validity!(project.root_mod) + + project + end + + defp check_app_name!(name, from_app_flag) do + unless name =~ Regex.recompile!(~r/^[a-z][a-z0-9_]*$/) do + extra = + if !from_app_flag do + ". The application name is inferred from the path, if you'd like to " <> + "explicitly name the application then use the `--app APP` option." + else + "" + end + + Mix.raise( + "Application name must start with a letter and have only lowercase " <> + "letters, numbers and underscore, got: #{inspect(name)}" <> extra + ) + end + end + + defp check_module_name_validity!(name) do + unless inspect(name) =~ Regex.recompile!(~r/^[A-Z]\w*(\.[A-Z]\w*)*$/) do + Mix.raise( + "Module name must be a valid Elixir alias (for example: Foo.Bar), got: #{inspect(name)}" + ) + end + end - igniter_generator.generate(igniter, project) + defp elixir_version_check! do + unless Version.match?(System.version(), "~> 1.15") do + Mix.raise( + "mix igniter.install_phoenix requires at least Elixir v1.15\n " <> + "You have #{System.version()}. Please update accordingly." + ) + end end end diff --git a/mix.exs b/mix.exs index 41c5d12..4fd9c21 100644 --- a/mix.exs +++ b/mix.exs @@ -100,7 +100,7 @@ defmodule Igniter.MixProject do {:spitfire, "~> 0.1 and >= 0.1.3"}, {:sourceror, "~> 1.4"}, {:jason, "~> 1.4"}, - {:phx_new, "~> 1.7", runtime: false}, + {:phx_new, "~> 1.7", optional: true}, # Dev/Test dependencies {:eflame, "~> 1.0", only: [:dev, :test]}, {:ex_doc, "~> 0.32", only: [:dev, :test], runtime: false}, diff --git a/mix.lock b/mix.lock index d0ee40a..d1c3086 100644 --- a/mix.lock +++ b/mix.lock @@ -24,6 +24,7 @@ "mix_audit": {:hex, :mix_audit, "2.1.4", "0a23d5b07350cdd69001c13882a4f5fb9f90fbd4cbf2ebc190a2ee0d187ea3e9", [:make, :mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.11", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "fd807653cc8c1cada2911129c7eb9e985e3cc76ebf26f4dd628bb25bbcaa7099"}, "mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, + "phx_new": {:hex, :phx_new, "1.7.14", "30d2d38b78bb762452595fe2e32f3a4a838f26e87713024840059884204ff141", [:mix], [], "hexpm", "e1a8b3839a9a2d94bceb95d96ca1f175264c751405fee8f39a4e67b379314a39"}, "rewrite": {:hex, :rewrite, "1.0.1", "2a249d703e47c050ad251fa43a3d019d4c08159ead95ec30ef48357ba88af609", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "79869f0bdb22840cf233b99e0dc7b6682a35d7e4747bdf2e78d3bc156b2c7c14"}, "sourceror": {:hex, :sourceror, "1.7.1", "599d78f4cc2be7d55c9c4fd0a8d772fd0478e3a50e726697c20d13d02aa056d4", [:mix], [], "hexpm", "cd6f268fe29fa00afbc535e215158680a0662b357dc784646d7dff28ac65a0fc"}, "spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"}, diff --git a/test/mix/tasks/igniter.install_phoenix_test.exs b/test/mix/tasks/igniter.install_phoenix_test.exs index 1e65f57..b73b3f2 100644 --- a/test/mix/tasks/igniter.install_phoenix_test.exs +++ b/test/mix/tasks/igniter.install_phoenix_test.exs @@ -2,9 +2,24 @@ defmodule Mix.Tasks.Igniter.InstallPhoenixTest do use ExUnit.Case import Igniter.Test - test "config_inject" do + test "create files" do + igniter = Igniter.compose_task(test_project(), "igniter.install_phoenix", ["my_app"]) + assert Enum.count(igniter.rewrite.sources) == 47 + end + + test "inject config" do test_project() |> Igniter.compose_task("igniter.install_phoenix", ["my_app"]) - |> assert_creates("lib/my_app_web/components/core_components.ex") + |> assert_has_patch("config/dev.exs", """ + 22 | # Configure your database + 23 | config :my_app, MyApp.Repo, + 24 | username: "postgres", + 25 | password: "postgres", + 26 | hostname: "localhost", + 27 | database: "my_app_dev", + 28 | stacktrace: true, + 29 | show_sensitive_data_on_connection_error: true, + 30 | pool_size: 10 + """) end end From 7f2c38f068e597373f455471fabd4d9dc6de4bbe Mon Sep 17 00:00:00 2001 From: Leandro Pereira Date: Wed, 6 Nov 2024 17:15:23 -0500 Subject: [PATCH 5/5] Do not expand %Project{} to avoid compilation errors --- lib/mix/tasks/igniter.install_phoenix.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/tasks/igniter.install_phoenix.ex b/lib/mix/tasks/igniter.install_phoenix.ex index 9a4d1db..9f72498 100644 --- a/lib/mix/tasks/igniter.install_phoenix.ex +++ b/lib/mix/tasks/igniter.install_phoenix.ex @@ -138,7 +138,7 @@ defmodule Mix.Tasks.Igniter.InstallPhoenix do |> igniter_generator.generate(project) end - defp validate_project(%Phx.New.Project{opts: opts} = project) do + defp validate_project(%{opts: opts} = project) do check_app_name!(project.app, !!opts[:app]) check_module_name_validity!(project.root_mod)