Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Igniter task to install Phoenix #140

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 51 additions & 36 deletions lib/igniter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1123,48 +1123,62 @@ 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
Rewrite.Source.from?(source, :string) &&
String.valid?(Rewrite.Source.get(source, :content)) ->
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

String.valid?(Rewrite.Source.get(source, :content)) ->
diff = Rewrite.Source.diff(source, color: color?) |> IO.iodata_to_binary()

if String.trim(diff) != "" do
"""
Update: #{Rewrite.Source.get(source, :path)}

if String.trim(diff) != "" do
#{diff}
"""
else
""
end

!String.valid?(Rewrite.Source.get(source, :content)) ->
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some files do not have a valid string content, for eg binary files like favicon.ico but I think we still need to display that such file will be created. Might need to improve this code to detect updates as well.

"""
Update: #{Rewrite.Source.get(source, :path)}
Create: #{Rewrite.Source.get(source, :path)}

#{diff}
(content diff can't be displayed)
"""
else

:else ->
dbg(source)
""
end
end
end)
end
Expand Down Expand Up @@ -1417,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()
leandrocp marked this conversation as resolved.
Show resolved Hide resolved
|> remove_unchanged_files()
|> then(fn igniter ->
if needs_test_support? do
Expand Down
117 changes: 117 additions & 0 deletions lib/igniter/phoenix/generator.ex
Original file line number Diff line number Diff line change
@@ -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)
leandrocp marked this conversation as resolved.
Show resolved Hide resolved

: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
71 changes: 71 additions & 0 deletions lib/igniter/phoenix/single.ex
Original file line number Diff line number Diff line change
@@ -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},

Check warning on line 14 in lib/igniter/phoenix/single.ex

View workflow job for this annotation

GitHub Actions / ash-ci / mix dialyzer

unknown_function

Function Phx.New.Project.ecto?/1 does not exist.
{Project.html?(project), &gen_html/2},

Check warning on line 15 in lib/igniter/phoenix/single.ex

View workflow job for this annotation

GitHub Actions / ash-ci / mix dialyzer

unknown_function

Function Phx.New.Project.html?/1 does not exist.
{Project.mailer?(project), &gen_mailer/2},

Check warning on line 16 in lib/igniter/phoenix/single.ex

View workflow job for this annotation

GitHub Actions / ash-ci / mix dialyzer

unknown_function

Function Phx.New.Project.mailer?/1 does not exist.
{Project.gettext?(project), &gen_gettext/2},

Check warning on line 17 in lib/igniter/phoenix/single.ex

View workflow job for this annotation

GitHub Actions / ash-ci / mix dialyzer

unknown_function

Function Phx.New.Project.gettext?/1 does not exist.
{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)

Check warning on line 50 in lib/igniter/phoenix/single.ex

View workflow job for this annotation

GitHub Actions / ash-ci / mix dialyzer

unknown_function

Function Phx.New.Project.javascript?/1 does not exist.
css? = Project.css?(project)

Check warning on line 51 in lib/igniter/phoenix/single.ex

View workflow job for this annotation

GitHub Actions / ash-ci / mix dialyzer

unknown_function

Function Phx.New.Project.css?/1 does not exist.
html? = Project.html?(project)

Check warning on line 52 in lib/igniter/phoenix/single.ex

View workflow job for this annotation

GitHub Actions / ash-ci / mix dialyzer

unknown_function

Function Phx.New.Project.html?/1 does not exist.

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
3 changes: 2 additions & 1 deletion lib/igniter/project/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,9 @@ defmodule Igniter.Project.Config do
end)
end

@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
Expand Down
51 changes: 51 additions & 0 deletions lib/mix/tasks/igniter.install_phoenix.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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) do
# TODO: check elixir version - https://github.com/phoenixframework/phoenix/blob/7586cbee9e37afbe0b3cdbd560b9e6aa60d32bf6/installer/lib/mix/tasks/phx.new.ex#L380
leandrocp marked this conversation as resolved.
Show resolved Hide resolved

%{base_path: base_path} = igniter.args.positional

# 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

Check warning on line 44 in lib/mix/tasks/igniter.install_phoenix.ex

View workflow job for this annotation

GitHub Actions / ash-ci / mix dialyzer

unknown_function

Function Phx.New.Project.new/2 does not exist.
|> Phx.New.Project.new(opts)
|> phx_generator.prepare_project()

Check warning on line 46 in lib/mix/tasks/igniter.install_phoenix.ex

View workflow job for this annotation

GitHub Actions / ash-ci / mix dialyzer

unknown_function

Function Phx.New.Generator.put_binding/1 does not exist.
|> Phx.New.Generator.put_binding()

igniter_generator.generate(igniter, project)
end
end
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +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},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this okay?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 This one is tough. I don't actually think we need this. IIRC phx_new will be available as long as the user has the archive installed? Which could mean that we can just at the start say "please install phx_new to use this task" if the module is not defined.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we could have it as a test only dependency in that case.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't actually think we need this. IIRC phx_new will be available as long as the user has the archive installed? Which could mean that we can just at the start say "please install phx_new to use this task" if the module is not defined.

Should we have it as an optional dependency, in that case? That seems like the best of both worlds: It doesn't become a transient dependency that all apps using Igniter are required to fetch, but it still allows Igniter to test against it and specify a version spec that Igniter's compatible with.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 I think that would make sense, yes. Since we'd be switching in our code on the module being compiled, it wouldn't matter if it's from a dep or the archive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to optional: true and added a check. Tested in a project, it will ask to install phx_new if not available.

# Dev/Test dependencies
{:eflame, "~> 1.0", only: [:dev, :test]},
{:ex_doc, "~> 0.32", only: [:dev, :test], runtime: false},
Expand Down
1 change: 1 addition & 0 deletions mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
10 changes: 10 additions & 0 deletions test/mix/tasks/igniter.install_phoenix_test.exs
Original file line number Diff line number Diff line change
@@ -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("lib/my_app_web/components/core_components.ex")
end
end
Loading