Igniter - A code generation and project patching framework

Igniter

Igniter is a code generation and project patching framework.

For library authors, this is a tool kit for writing smarter generators that can semantically modify existing files, and all sorts of useful tools for doing so.

For end-users, this means mix igniter.install <package>, which will add it to your mix.exs automatically and then run that library’s installer if it has one. Even when libraries don’t have an installer, or use igniter, this behavior makes it useful to keep around.

Installation

Igniter can be added to an existing elixir project by adding it to your dependencies:

{:igniter, "~> 0.1", only: [:dev]}

You can also generate new projects with igniter preinstalled, and run installers in the same command.

mix igniter.new app_name --install ash

To use this command, install the archive:

mix archive.install hex igniter_new

Patterns

Mix tasks built with igniter are both individually callable, and composable. This means that tasks can call eachother, and also end users can create and customize their own generators composing existing tasks.

Installers

Igniter will look for a task called <your_package>.install when the user runs mix igniter.install <your_package>, and will run it after installing and fetching dependencies.

Generators/Patchers

These can be run like any other mix task, or composed together. For example, lets say that you wanted to have your own Ash.Resource generator, that starts with the default mix ash.gen.resource task, but then adds or modifies files:

# in lib/mix/tasks/my_app.gen.resource.ex
defmodule Mix.Tasks.MyApp.Gen.Resource do
  use Igniter.Mix.Task

  def igniter(igniter, [resource | _] = argv) do
    resource = Igniter.Code.Module.parse(resource)
    my_special_thing = Module.concat([resource, SpecialThing])
    location = Igniter.Code.Module.proper_location(my_special_thing)

    igniter
    |> Igniter.compose_task("ash.gen.resource", argv)
    |> Igniter.create_new_elixir_file(location, """
    defmodule #{inspect(my_special_thing)} do
      # this is the special thing for #{inspect()}
    end
    """)
  end
end
33 Likes

I’m so excited to see where this tool goes!

2 Likes

https://hexdocs.pm/igniter

:slight_smile:

PS. The Hex link in the badge in the GitHub README has an extra h in it.

1 Like

This looks super sweet!

I’m making a package that chains mix tasks to generate projects from blueprints, configured from livebook smart cells.

I’ll definitely be adding Igniter to the system. If only you’d published this earlier I would’ve had a lot fewer headaches :slightly_smiling_face: :rofl:

1 Like

Fixed! And thanks for posting the links. In the guide lines it says not to link to external sources but I think I took that too literally. It means “explain your package here”, not “don’t have any links pointing elsewhere” :laughing:

With mix igniter.install, you can install multiple packages at once! Use the @ symbol to specify specific versions, git/GitHub dependencies, or even path dependencies!

2 Likes

:clap: Kudos to @zachdaniel and everyone else that has been contributing to the Ash Framework. However you feel about adopting Ash itself, so many great libraries keep spinning out of that effort!

(If you haven’t yet, also check out Spark.)

3 Likes

Hi @zachdaniel I wanted to ask if I am trying to generate a directory of Ecto.Schema models in my project based on an open-api specification, is Igniter a good fit? Is it an intended use?

If the api changes, I would blow away and regenerate those models.

In that case, you likely do not need igniter. Only if you wanted to support changes being made to the schema files and retaining them while patching in new updates from the open api spec.

1 Like

Awesome work! Can’t wait to try it out and try to replace some of the handwritten generators I have in our mix directory like this one operately/lib/mix/tasks/operately.gen.api.query.ex at main · operately/operately · GitHub.

Hopefully, it could be just a drop-in replacement for what we already have, but with a polished and well thought through system.

1 Like

Hey @zachdaniel! I’m finally taking Igniter for a spin on a set of component generators. There is already a Mix.Task that takes some EEx modules & HEEx templates and injects them into an application. It’ll be interesting to refactor that task to use Igniter.

Where I’m stuck, though, is on a task for adding deps to mix.exs. I added :igniter as a dependency in the generator repo (Constructor), and used mix igniter.gen.task your_app.task.name to generate the task code. After building and installing the archive, I run the new task and get an error:

~/projects/yolo  constructor_components ✔                                                                                            12d0h  
▶ mix constructor.install.deps
** (UndefinedFunctionError) function Igniter.Mix.Task.Info.global_options/0 is undefined (module Igniter.Mix.Task.Info is not available)
    Igniter.Mix.Task.Info.global_options()
    lib/mix/tasks/constructor.install.deps.ex:2: Mix.Tasks.Constructor.Install.Deps.run/1
    (mix 1.17.2) lib/mix/task.ex:495: anonymous fn/3 in Mix.Task.run_task/5
    (mix 1.17.2) lib/mix/cli.ex:96: Mix.CLI.run_task/2
    /Users/owen/.asdf/installs/elixir/1.17.2-otp-27/bin/mix:2: (file)
  • Inside Constructor’s mix.exs, do I need anything more than {:igniter, "~> 0.3"}?
  • Does Igniter need to be a dependency in the Yolo app?

This was a bug that was just fixed. Update all your ash/igniter deps, including the install of igniter.

1 Like

I’m still getting the same error using 0.3.62 and the git repo. Igniter.Mix.Task.Info is obviously in the repo, so I don’t see why it’s not found when I try to use the task.

:thinking: you’re trying to create an archive that depends on igniter? hex archives can’t have dependencies, unfortunately, so yes igniter would need to be a dependency of the target application.

EDIT: if I’m understanding what you’re trying to do.

1 Like

Ok, this makes sense now. Yeah, the idea is to have a repo of tasks that could be run in new and existing projects. When I add the repo as a path dependency to the sandbox app, I can finally run the igniter task.

:thinking: :thinking: :thinking: I wonder if it would be a bad idea to Mix.install Igniter with @before_compile or the like.

Hm…I don’t think that would work? Because Mix.install shouts if Mix is available IIRC. It would be very interesting if you find a way to do that, however. I’d probably even do the same thing so that mix igniter.install can work even without the dependency being present.

1 Like

Yeah, Mix.install appears to be a no-go. Since Constructor is going to be a collection of generators, I hesitate to make it a dependency within each project.

I think the better approach for this thing may be to have devs clone the repo and run tasks from the constructor directory. The generators will need to take a path arg to locate the target project directory, but that seems manageable.

Honestly, for Ash, Beacon, etc and many other packages, we’re just making igniter a dependency of the project. I anticipate this to eventually be practically standard (maybe hubris :man_shrugging:). So maybe you can just detect that they don’t have igniter as a dep of their project and tell them that they need to add it if they don’t have it? They can even remove it when theyre done if they want.

and honestly if you make your generator lib a dev only, runtime false dependency in their project, its effectively zero cost for them, or about as close as it gets. And they can always remove your dependency when they’re done as well. That sounds much nicer than having to clone the repo to me.

That’s fair. One potential use case for us, though, is generating new projects with some common defaults. Unless that changes, I think cloned repo seems necessary. This is a loosely held strong opinion, so it could change tomorrow.