Composer.Templates (composer v0.2.0)

Copy Markdown View Source

Helpers for steps that write files from templates.

A step's templates live beside it, in the directory named after its file with a leading underscore (Composer.Step.template_dir/1):

lib/composer/core/steps/sidebar_layout.ex
lib/composer/core/steps/_sidebar_layout/app_fn.ex.eex
lib/composer/core/steps/_sidebar_layout/files/...

Inside a step, template/1 (imported by use Composer.Step) refers to a file or directory there, and is checked at compile time:

def igniter(igniter) do
  igniter
  |> Composer.Templates.require_present({:dep, :heroicons}, "the sidebar layout uses heroicons")
  |> Composer.Templates.copy_dir(template("files"), ".")
  |> Composer.Templates.replace_function(
    Module.concat([Composer.Templates.assigns(igniter).web_module, "Layouts"]),
    :app,
    1,
    template("app_fn.ex.eex")
  )
end

A reference is {cookbook, step_id, path}. Code outside a step, such as a helper module shared by a cookbook's steps, builds one with ref/2.

Rendering

Only files ending in .eex are rendered, with EEx, and written without the .eex extension. Every other file is copied verbatim, so templates that contain their own <%= %> tags (a HEEx or Astral template) are safe. Elixir source must be stored as .ex.eex: a bare .ex file in a step's directory is a child step, compiled as part of the package, so use Composer.Cookbook rejects one that is not a step.

.eex templates see the assigns as @name. assigns/1 builds the standard ones, and every helper merges caller-given assigns over them.

Path variables

A path segment such as __web_dir__ is replaced with the matching assign, so lib/__web_dir__/components/layouts.ex.eex is written to lib/my_app_web/components/layouts.ex. A variable with no assign raises, naming the variable and the template; nothing is written with a literal __var__ in its path.

Existing files

A target file that exists is overwritten by default; the change is still shown in the Igniter diff before anything is written. Pass on_exists: :skip to leave existing files alone.

Summary

Types

A template: {cookbook, step_id, path}, path relative to the step's template directory.

Functions

Returns the standard assigns for the target app

Writes every file under a template directory into target_dir, keeping the relative layout. .gitkeep files are skipped. Options are as for copy_file/4.

Writes one template to target, rendering it when it ends in .eex.

Replaces __var__ path variables in path with assigns, raising with the variable and template when one has no assign.

Adds content to the file at path

Returns the absolute path of a template, raising when its cookbook or the file does not exist.

Returns a reference to path in the template directory of step, a step module.

Renders a template file: EEx when it ends in .eex, verbatim otherwise.

Replaces function name/arity of module with the rendered template, a complete def.

Adds a warning when a prerequisite is missing: {:dep, name} for a dependency in mix.exs, or {:file, path}.

Types

template_ref()

@type template_ref() :: {atom() | String.t(), String.t(), String.t()}

A template: {cookbook, step_id, path}, path relative to the step's template directory.

Functions

assigns(igniter)

@spec assigns(Igniter.t()) :: map()

Returns the standard assigns for the target app:

%{app_name: "my_app", app_module: "MyApp", web_module: "MyAppWeb", web_dir: "my_app_web"}

copy_dir(igniter, template_ref, target_dir, opts \\ [])

@spec copy_dir(Igniter.t(), template_ref(), Path.t(), keyword()) :: Igniter.t()

Writes every file under a template directory into target_dir, keeping the relative layout. .gitkeep files are skipped. Options are as for copy_file/4.

copy_file(igniter, template_ref, target, opts \\ [])

@spec copy_file(Igniter.t(), template_ref(), Path.t(), keyword()) :: Igniter.t()

Writes one template to target, rendering it when it ends in .eex.

target may contain path variables, and loses a trailing .eex.

Options

  • :assigns - merged over assigns/1
  • :on_exists - :overwrite (default) or :skip

expand_path(path, assigns, template)

@spec expand_path(Path.t(), map(), Path.t()) :: Path.t()

Replaces __var__ path variables in path with assigns, raising with the variable and template when one has no assign.

insert(igniter, path, content, opts)

@spec insert(Igniter.t(), Path.t(), String.t(), keyword()) :: Igniter.t()

Adds content to the file at path:

  • at: {:after, anchor} - right after the first occurrence of anchor
  • at: {:before, anchor} - right before it
  • at: :append - at the end of the file

Content already in the file is not added again. A missing file or anchor is a loud warning rather than a silent no-op, so drift in the target surfaces. Pass warning: to word it.

path(arg)

@spec path(template_ref()) :: Path.t()

Returns the absolute path of a template, raising when its cookbook or the file does not exist.

ref(step, path)

@spec ref(module(), Path.t()) :: template_ref()

Returns a reference to path in the template directory of step, a step module.

This is how code outside a step refers to a step's templates:

Composer.Templates.ref(Composer.ComposerLab.Steps.Astral.DaisyComponents, "components/button.astral")
#=> {"composer_lab", "astral/daisy_components", "components/button.astral"}

Inside a step, use template/1, which is also checked at compile time. Raises when step is not a step module.

render(source, assigns)

@spec render(Path.t(), map()) :: String.t()

Renders a template file: EEx when it ends in .eex, verbatim otherwise.

replace_function(igniter, module, name, arity, template_ref, assigns \\ %{}, opts \\ [])

@spec replace_function(
  Igniter.t(),
  module(),
  atom(),
  arity(),
  template_ref(),
  map() | keyword(),
  keyword()
) :: Igniter.t()

Replaces function name/arity of module with the rendered template, a complete def.

A missing function or module is a warning, not a crash, since the target app may have been customized. Pass warning: in opts to word it.

require_present(igniter, check, reason)

@spec require_present(Igniter.t(), {:dep, atom()} | {:file, Path.t()}, String.t()) ::
  Igniter.t()

Adds a warning when a prerequisite is missing: {:dep, name} for a dependency in mix.exs, or {:file, path}.