improvement: add installer archive

This commit is contained in:
Zach Daniel 2024-06-03 13:13:49 -04:00
parent d8b463343c
commit f1e0fffc4b
11 changed files with 359 additions and 187 deletions

27
.gitignore vendored
View file

@ -24,3 +24,30 @@ igniter-*.tar
# Temporary files, for example, from tests.
/tmp/
# The directory Mix will write compiled artifacts to.
/installer/_build/
# If you run "mix test --cover", coverage assets end up here.
/installer/cover/
# The directory Mix downloads your dependencies sources to.
/installer/deps/
# Where third-party dependencies like ExDoc output generated docs.
/installer/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/installer/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
installer/erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
igniter-*.tar
# Temporary files, for example, from tests.
installer/tmp/

17
installer/README.md Normal file
View file

@ -0,0 +1,17 @@
## mix igniter.new
Provides `igniter.new` installer as an archive.
To install from Hex, run:
$ mix archive.install hex igniter_new
To build and install it locally,
ensure any previous archive versions are removed:
$ mix archive.uninstall phx_new
Then run:
$ cd installer
$ MIX_ENV=prod mix do archive.build, archive.install

View file

@ -0,0 +1,87 @@
defmodule Mix.Tasks.Igniter.New do
use Mix.Task
@igniter_version Mix.Project.config()[:version]
@shortdoc "Creates a new Igniter application"
def run([name | _ ] = argv) do
{options, argv, _errors} = OptionParser.parse(argv,
strict: [install: :keep, local: :string, example: :boolean],
aliases: [i: :install, l: :local, e: :example]
)
install =
options[:install]
|> List.wrap()
|> Enum.join(",")
|> String.split(",", trim: true)
if File.exists?(name) do
Mix.shell().error("""
The directory #{name} already exists. You must either:
1. remove or move it
2. If you are trying to modify an existing project add `{:igniter` to the project, if it is not
already added, and then run `mix igniter.install #{Enum.join(install, ",")}` inside the project
""")
exit({:shutdown, 1})
end
exit = Mix.shell().cmd("mix new #{Enum.join(argv, " ")}")
if exit == 0 do
version_requirement =
if options[:local] do
local = Path.join(["..", Path.relative_to_cwd(options[:local])])
"path: #{inspect(local)}"
else
inspect(version_requirement())
end
File.cd!(name)
contents =
"mix.exs"
|> File.read!()
if String.contains?(contents, "{:igniter") do
Mix.shell().info("It looks like the project already exists and igniter is already installed, not adding it to deps.")
else
new_contents =
String.replace(contents, "defp deps do\n [\n", "defp deps do\n [\n{:igniter, #{version_requirement}}\n")
File.write!("mix.exs", new_contents)
end
Mix.shell().cmd("mix deps.get")
Mix.shell().cmd("mix compile")
unless Enum.empty?(install) do
example =
if options[:example] do
"--example"
end
Mix.shell().cmd("mix igniter.install #{Enum.join(install, ",")} --yes #{example}" |> IO.inspect())
end
else
Mix.shell().info("Aborting command because associated `mix new` command failed.")
exit({:shutdown, 1})
end
:ok
end
defp version_requirement do
@igniter_version
|> Version.parse!()
|> case do
%Version{major: 0, minor: minor} ->
"~> 0.#{minor}"
%Version{major: major} ->
"~> #{major}.0"
end
end
end

41
installer/mix.exs Normal file
View file

@ -0,0 +1,41 @@
defmodule Igniter.New.MixProject do
use Mix.Project
@version "0.1.0"
@scm_url "https://github.com/ash-project/igniter"
def project do
[
app: :igniter_new,
start_permanent: Mix.env() == :prod,
version: @version,
elixir: "~> 1.14",
deps: deps(),
package: [
maintainers: ["Zach Daniel"],
licenses: ["MIT"],
links: %{"GitHub" => @scm_url},
files: ~w(lib templates mix.exs README.md)
],
preferred_cli_env: [docs: :docs],
source_url: @scm_url,
docs: docs(),
homepage_url: "https://www.ash-hq.org",
description: """
Create a new mix project with igniter, and run igniter installers in one command!
"""
]
end
def deps do
[
{:ex_doc, "~> 0.24", only: :docs}
]
end
defp docs do
[
source_url_pattern: "#{@scm_url}/blob/v#{@version}/installer/%{path}#L%{line}"
]
end
end

8
installer/mix.lock Normal file
View file

@ -0,0 +1,8 @@
%{
"earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"},
"ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"},
"makeup": {:hex, :makeup, "1.1.2", "9ba8837913bdf757787e71c1581c21f9d2455f4dd04cfca785c70bbfff1a76a3", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cce1566b81fbcbd21eca8ffe808f33b221f9eee2cbc7a1706fc3da9ff18e6cac"},
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
}

View file

@ -12,18 +12,15 @@ defmodule Igniter.Config do
igniter
|> Igniter.include_or_create_elixir_file(file_path, "import Config\n")
|> Igniter.update_file(file_path, fn source ->
quoted = Rewrite.Source.get(source, :quoted)
zipper = Zipper.zip(quoted)
|> Igniter.update_elixir_file(file_path, fn zipper ->
case try_update_three_arg(zipper, config_path, app_name, updater) do
{:ok, zipper} ->
Rewrite.Source.update(source, :configure, :quoted, Zipper.root(zipper))
zipper
:error ->
case try_update_two_arg(zipper, config_path, app_name, value, updater) do
{:ok, zipper} ->
Rewrite.Source.update(source, :configure, :quoted, Zipper.root(zipper))
zipper
:error ->
# add new code here
@ -32,17 +29,7 @@ defmodule Igniter.Config do
config =
{:config, [], [app_name, [{first, Igniter.Common.keywordify(rest, value)}]]}
code =
zipper
|> Igniter.Common.add_code(config)
|> Zipper.root()
Rewrite.Source.update(
source,
:configure,
:quoted,
code
)
Igniter.Common.add_code(zipper, config)
end
end
end)

View file

@ -63,70 +63,40 @@ defmodule Igniter.Deps do
defp remove_dependency(igniter, name) do
igniter
|> Igniter.update_file("mix.exs", fn source ->
quoted = Rewrite.Source.get(source, :quoted)
new_quoted =
with zipper <- Zipper.zip(quoted),
{:ok, zipper} <- Common.move_to_module_using(zipper, Mix.Project),
{:ok, zipper} <- Common.move_to_defp(zipper, :deps, 0),
true <- Common.node_matches_pattern?(zipper, value when is_list(value)),
current_declaration_index when not is_nil(current_declaration_index) <-
Common.find_list_item_index(zipper, fn item ->
if Common.tuple?(item) do
first_elem = Common.tuple_elem(item, 0)
first_elem && Common.node_matches_pattern?(first_elem, ^name)
end
end) do
zipper
|> Common.remove_index(current_declaration_index)
|> Zipper.root()
else
_ ->
quoted
end
if new_quoted == quoted do
Rewrite.Source.add_issue(
source,
"Failed to remove dependency #{inspect(name)}"
)
|> Igniter.update_elixir_file("mix.exs", fn zipper ->
with {:ok, zipper} <- Common.move_to_module_using(zipper, Mix.Project),
{:ok, zipper} <- Common.move_to_defp(zipper, :deps, 0),
true <- Common.node_matches_pattern?(zipper, value when is_list(value)),
current_declaration_index when not is_nil(current_declaration_index) <-
Common.find_list_item_index(zipper, fn item ->
if Common.tuple?(item) do
first_elem = Common.tuple_elem(item, 0)
first_elem && Common.node_matches_pattern?(first_elem, ^name)
end
end) do
Common.remove_index(zipper, current_declaration_index)
else
Rewrite.Source.update(source, :add_dependency, :quoted, new_quoted)
_ ->
{:error, "Failed to remove dependency #{inspect(name)}"}
end
end)
end
defp do_add_dependency(igniter, name, version) do
igniter
|> Igniter.update_file("mix.exs", fn source ->
quoted = Rewrite.Source.get(source, :quoted)
|> Igniter.update_elixir_file("mix.exs", fn zipper ->
with {:ok, zipper} <- Common.move_to_module_using(zipper, Mix.Project),
{:ok, zipper} <- Common.move_to_defp(zipper, :deps, 0),
true <- Common.node_matches_pattern?(zipper, value when is_list(value)) do
quoted =
quote do
{unquote(name), unquote(version)}
end
new_quoted =
with zipper <- Zipper.zip(quoted),
{:ok, zipper} <- Common.move_to_module_using(zipper, Mix.Project),
{:ok, zipper} <- Common.move_to_defp(zipper, :deps, 0),
true <- Common.node_matches_pattern?(zipper, value when is_list(value)) do
quoted =
quote do
{unquote(name), unquote(version)}
end
zipper
|> Common.prepend_to_list(quoted)
|> Zipper.root()
else
_ ->
quoted
end
if new_quoted == quoted do
Rewrite.Source.add_issue(
source,
"Failed to add dependency #{inspect({inspect(name), inspect(version)})}"
)
Common.prepend_to_list(zipper, quoted)
else
Rewrite.Source.update(source, :add_dependency, :quoted, new_quoted)
_ ->
{:error, "Failed to add dependency #{inspect({inspect(name), inspect(version)})}"}
end
end)
end

View file

@ -13,87 +13,72 @@ defmodule Igniter.Formatter do
def import_dep(igniter, dep) do
igniter
|> Igniter.include_or_create_elixir_file(".formatter.exs", @default_formatter)
|> Igniter.update_file(".formatter.exs", fn source ->
quoted = Rewrite.Source.get(source, :quoted)
zipper = Zipper.zip(quoted)
new_code =
zipper
|> Zipper.down()
|> case do
nil ->
code =
quote do
[import_deps: [unquote(dep)]]
end
zipper
|> Igniter.Common.add_code(code)
zipper ->
zipper
|> Zipper.rightmost()
|> Common.put_in_keyword([:import_deps], [dep], fn nested_zipper ->
Igniter.Common.prepend_new_to_list(
nested_zipper,
dep
)
end)
|> case do
{:ok, zipper} ->
zipper
:error ->
zipper
|> Igniter.update_elixir_file(".formatter.exs", fn zipper ->
zipper
|> Zipper.down()
|> case do
nil ->
code =
quote do
[import_deps: [unquote(dep)]]
end
end
|> Zipper.root()
Rewrite.Source.update(source, :import_formatter_dep, :quoted, new_code)
Igniter.Common.add_code(zipper, code)
zipper ->
zipper
|> Zipper.rightmost()
|> Common.put_in_keyword([:import_deps], [dep], fn nested_zipper ->
Igniter.Common.prepend_new_to_list(
nested_zipper,
dep
)
end)
|> case do
{:ok, zipper} ->
zipper
:error ->
zipper
end
end
end)
end
def add_formatter_plugin(igniter, plugin) do
igniter
|> Igniter.include_or_create_elixir_file(".formatter.exs", @default_formatter)
|> Igniter.update_file(".formatter.exs", fn source ->
quoted = Rewrite.Source.get(source, :quoted)
zipper = Zipper.zip(quoted)
new_code =
zipper
|> Zipper.down()
|> case do
nil ->
code =
quote do
[plugins: [unquote(plugin)]]
end
zipper
|> Igniter.Common.add_code(code)
zipper ->
zipper
|> Zipper.rightmost()
|> Common.put_in_keyword([:plugins], [Spark.Formatter], fn nested_zipper ->
Igniter.Common.prepend_new_to_list(
nested_zipper,
Spark.Formatter,
&Igniter.Common.equal_modules?/2
)
end)
|> case do
{:ok, zipper} ->
zipper
_ ->
zipper
|> Igniter.update_elixir_file(".formatter.exs", fn zipper ->
zipper
|> Zipper.down()
|> case do
nil ->
code =
quote do
[plugins: [unquote(plugin)]]
end
end
|> Zipper.root()
Rewrite.Source.update(source, :add_formatter_plugin, :quoted, new_code)
zipper
|> Igniter.Common.add_code(code)
zipper ->
zipper
|> Zipper.rightmost()
|> Common.put_in_keyword([:plugins], [Spark.Formatter], fn nested_zipper ->
Igniter.Common.prepend_new_to_list(
nested_zipper,
Spark.Formatter,
&Igniter.Common.equal_modules?/2
)
end)
|> case do
{:ok, zipper} ->
zipper
_ ->
zipper
end
end
end)
end
end

View file

@ -53,6 +53,32 @@ defmodule Igniter do
end
end
def update_elixir_file(igniter, path, func) do
if Rewrite.has_source?(igniter.rewrite, path) do
%{
igniter
| rewrite:
Rewrite.update!(igniter.rewrite, path, fn source ->
apply_func_with_zipper(source, func)
end)
}
else
if File.exists?(path) do
source = Rewrite.Source.Ex.read!(path)
%{igniter | rewrite: Rewrite.put!(igniter.rewrite, source)}
|> format(path)
|> Map.update!(:rewrite, fn rewrite ->
Rewrite.update!(rewrite, path, fn source ->
apply_func_with_zipper(source, func)
end)
end)
else
add_issue(igniter, "Required #{path} but it did not exist")
end
end
end
def update_file(igniter, path, func) do
if Rewrite.has_source?(igniter.rewrite, path) do
%{igniter | rewrite: Rewrite.update!(igniter.rewrite, path, func)}
@ -161,50 +187,52 @@ defmodule Igniter do
end)
|> case do
[] ->
unless opts[:quiet_on_no_changes?] do
unless opts[:quiet_on_no_changes?] || "--yes" in argv do
Mix.shell().info("\n#{title}: No proposed changes!\n")
end
:dry_run_with_no_changes
sources ->
Mix.shell().info("\n#{title}: Proposed changes:\n")
if "--dry-run" in argv || "--yes" not in argv do
Mix.shell().info("\n#{title}: Proposed changes:\n")
Enum.each(sources, fn source ->
if Rewrite.Source.from?(source, :string) do
content_lines =
source
|> Rewrite.Source.get(:content)
|> String.split("\n")
|> Enum.with_index()
Enum.each(sources, fn source ->
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()
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
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)} #{IO.ANSI.yellow()}| #{IO.ANSI.green()}#{line}#{IO.ANSI.reset()}"
end)
"#{String.pad_trailing(to_string(line_number), space_padding)} #{IO.ANSI.yellow()}| #{IO.ANSI.green()}#{line}#{IO.ANSI.reset()}"
end)
Mix.shell().info("""
Create: #{Rewrite.Source.get(source, :path)}
Mix.shell().info("""
Create: #{Rewrite.Source.get(source, :path)}
#{diffish_looking_text}
""")
else
Mix.shell().info("""
Update: #{Rewrite.Source.get(source, :path)}
#{diffish_looking_text}
""")
else
Mix.shell().info("""
Update: #{Rewrite.Source.get(source, :path)}
#{Rewrite.Source.diff(source)}
""")
end
end)
#{Rewrite.Source.diff(source)}
""")
end
end)
end
:dry_run_with_changes
end
@ -439,4 +467,22 @@ defmodule Igniter do
opts
end
defp apply_func_with_zipper(source, func) do
quoted = Rewrite.Source.get(source, :quoted)
zipper = Sourceror.Zipper.zip(quoted)
case func.(zipper) do
%Sourceror.Zipper{} = zipper ->
Rewrite.Source.update(
source,
:configure,
:quoted,
Sourceror.Zipper.root(zipper)
)
{:error, error} ->
Rewrite.Source.add_issues(source, List.wrap(error))
end
end
end

View file

@ -1,7 +1,7 @@
defmodule Igniter.Install do
@moduledoc false
@option_schema [
switches: [
strict: [
example: :boolean,
dry_run: :boolean,
yes: :boolean
@ -19,8 +19,8 @@ defmodule Igniter.Install do
Application.ensure_all_started(:req)
{options, _} =
OptionParser.parse!(argv, @option_schema)
{options, _, _unprocessed_argv} =
OptionParser.parse(argv, @option_schema)
argv = OptionParser.to_argv(options)
@ -42,16 +42,7 @@ defmodule Igniter.Install do
| _
]
} ->
requirement =
version
|> Version.parse!()
|> case do
%Version{major: 0, minor: minor} ->
"~> 0.#{minor}"
%Version{major: major} ->
"~> #{major}.0"
end
requirement = Igniter.Version.version_string_to_general_requirement(version)
Igniter.Deps.add_dependency(igniter, install, requirement)

13
lib/version.ex Normal file
View file

@ -0,0 +1,13 @@
defmodule Igniter.Version do
def version_string_to_general_requirement(version) do
version
|> Version.parse!()
|> case do
%Version{major: 0, minor: minor} ->
"~> 0.#{minor}"
%Version{major: major} ->
"~> #{major}.0"
end
end
end