View Source Uniform.Blueprint behaviour (Uniform v0.6.0)

Defines the ejection blueprint for your project.

When used, the blueprint accepts the :templates option, which is not required. For example, the blueprint:

defmodule Blueprint do
  use Uniform.Blueprint, templates: "priv/uniform-templates"
end

Would search for templates in the priv/uniform-templates directory. See template/2 for more information.

the-base_files-section

The base_files Section

The base_files section specifies files that should be ejected which aren't in lib/my_app. (When running mix uniform.eject my_app.)

defmodule Blueprint do
  use Uniform.Blueprint

  base_files do
    file "my_main_app/application.ex"
    cp_r "assets"
    # ...
  end
end

See base_files/1 for more information.

files-that-are-always-ejected

Files that are always ejected

There are a handful of files that are automatically ejected. You do not need to specify these in the base_files section.

mix.exs
mix.lock
.gitignore
.formatter.exs
test/test_helper.exs

the-deps-section

The deps Section

Besides the base_files section, the blueprint can also contain a deps section to configure dependencies.

defmodule Blueprint do
  use Uniform.Blueprint

  deps do
    always do
      mix :phoenix
      lib :my_component_library
    end

    mix :absinthe do
      mix_deps [:absinthe_plug, :dataloader]
    end
  end
end

See deps/1 for more information.

modifying-files-programmatically-with-modify

Modifying files programmatically with modify

Lastly, modify can be used whenever you want to transform file contents during ejection. You can specify a specific filepath or use a regex to match multiple files.

defmodule Blueprint do
  use Uniform.Blueprint

  modify "assets/js/app.js", fn file, app ->
    String.replace(file, "SOME_VALUE_PER_APP", app.extra[:some_value])
  end

  modify ~r/_worker.ex/, &MyApp.Modify.modify_workers/1

See modify/2 for more information.

preserving-files

Preserving files

Before mix uniform.eject copies any files, the contents in the destination directory are deleted – except for the .git, deps, and _build directories.

If there are any other files or directories in the project's root folder that you would like to preserve (by not deleting them), specify them with @preserve.

# .env will not be deleted immediately before ejection
@preserve [
  ".env"
]

full-example

Full Example

Below is an example Blueprint module that shows off a majority of the features that can be used.

defmodule MyApp.Uniform.Blueprint do
  use Uniform.Blueprint, templates: "lib/uniform/templates"

  # do not delete this root file when clearing the destination
  @preserve [".env"]

  @impl Uniform.Blueprint
  def extra(app) do
    theme =
      case app.name.underscore do
        "empire_" <> _ -> :empire
        "rebel_" <> _ -> :rebel
      end

    # set `app.extra[:theme]`
    [theme: theme]
  end

  @impl Uniform.Blueprint
  def target_path(path, app) do
    if is_web_file?(path) do
      # modify the path to put it in `lib/some_app_web`
      String.replace(path, "lib/#{app.name.underscore}/", "lib/#{app.name.underscore}_web/")
    else
      path
    end
  end

  # files to eject in every app, which are outside `lib/that_app`
  base_files do
    # copy these directories wholesale; do NOT run them through code modifiers
    cp_r "assets"

    # eject these files which aren't in lib/the_app_directory
    file ".credo.exs"
    file ".github/workflows/elixir.yml"
    file "priv/static/#{app.extra[:theme]}-favicon.ico"
    file "lib/my_app_web.ex"

    # eject a file from an EEx template at "lib/uniform/templates/config/runtime.exs.eex"
    # configure the templates directory on line 2
    template "config/runtime.exs"

    # conditionally eject some files
    if deploys_to_aws?(app) do
      file "file/required/by/aws"
      template "dynamic/file/required/by/aws"
    end

    if depends_on?(app, :lib, :some_lib) do
      template "dynamic/file/required/by/some_lib"
    end
  end

  # run the file contents through this modifier if the file is ejected
  modify "lib/my_app_web/templates/layout/root.html.heex", file, app do
    file
    |> String.replace("empire-favicon.ico", "#{app.extra[:theme]}-favicon.ico")
    |> String.replace("empire-apple-touch-icon.png", "#{app.extra[:theme]}-apple-touch-icon.png")
  end

  # configure dependencies from mix.exs and `lib/`
  deps do
    # always eject the dependencies in the `always` section;
    # don't require adding them to uniform.exs
    always do
      lib :my_app do
        # only eject the following files in `lib/my_app`
        only ["lib/my_app/application.ex"]
      end

      lib :my_app_web do
        # only eject the following files in `lib/my_app_web`
        only [
          "lib/my_app_web/endpoint.ex",
          "lib/my_app_web/router.ex",
          "lib/my_app_web/channels/user_socket.ex",
          "lib/my_app_web/views/error_view.ex",
          "lib/my_app_web/templates/layout/root.html.heex",
          "lib/my_app_web/templates/layout/app.html.heex",
          "lib/my_app_web/templates/layout/live.html.heex"
        ]
      end

      # always include these mix dependencies
      mix :credo
      mix :ex_doc
      mix :phoenix
      mix :phoenix_html
    end

    # if absinthe is included, also include absinthe_plug and dataloader
    mix :absinthe do
      mix_deps [:absinthe_plug, :dataloader]
    end

    lib :my_data_lib do
      # if my_data_lib is included, also include other_lib, faker, and norm
      lib_deps [:other_lib]
      mix_deps [:faker, :norm]

      # if my_data_lib is included, also eject these files
      file "priv/my_data_repo/**/*.exs", match_dot: true
      file "test/support/fixtures/my_data_lib/**/*.ex"
    end
  end
end

Link to this section Summary

Callbacks

This callback works like the except/1 instruction for Lib Dependencies, except that it applies to the lib folder of the ejected app itself.

A callback to add data to app.extra.

Use this optional callback to change the path of files in the ejected codebase.

Functions

Inside of a deps do block, any Mix or Lib dependencies that should be included in every ejected app should be wrapped in an always do block

The base_files section is where you specify files outside of an ejected app's lib/my_app and test/my_app directories which should always be ejected.

cp works exactly like file/2, except that no Code Transformations are applied to the file.

cp_r works like cp/2, but for a directory instead of a file. The directory is copied as-is with File.cp_r!/3.

Uniform automatically catalogs all Mix deps by looking into mix.exs to discover all Mix deps. It also catalogs all Lib deps by scanning the lib/ directory.

In the deps section of your Blueprint, you can specify that a Lib Dependency excludes certain files.

In base_files and lib blocks, file is used to specify files that are not in a lib/ directory which should be ejected in the app or along with the lib.

Lib Dependencies are shared code libraries in the lib directory of your project. Uniform scans lib/, so you don't need to inform it about them.

Specify transitive Lib Dependencies of other Lib Dependencies.

Uniform automatically catalogues the deps in mix.exs, so there are only two scenarios where you need to list them in your Blueprint.

Specify transitive Mix Dependencies of other Lib/Mix Dependencies.

Modify the contents of one or more files during ejection.

In the deps section of your Blueprint, you can specify that a Lib Dependency only includes certain files.

In base_files and lib blocks, template is used to specify EEx templates that should be rendered and then ejected.

Link to this section Callbacks

Link to this callback

app_lib_except(app)

View Source (optional)
@callback app_lib_except(app :: Uniform.App.t()) :: [Path.t() | Regex.t()]

This callback works like the except/1 instruction for Lib Dependencies, except that it applies to the lib folder of the ejected app itself.

When running mix uniform.eject my_app, any files in lib/my_app or test/my_app which match the paths or regexes returned by app_lib_except will not be ejected.

def app_lib_except(app) do
  ["lib/#{app.name.underscore}/hidden_file.ex"]
end
@callback extra(app :: Uniform.App.t()) :: keyword()

A callback to add data to app.extra.

This callback exists for scenarios when you want to add an extra key to many or all apps, and it can be programmatically determined from the information in the app.

extra data that only applies to a single app usually belongs in the Uniform Manifest.

example

Example

You may want to set the theme based on the name of the ejectable app. Return a keyword list with a theme key. It will be available via app.extra[:theme] in modify/2, base_files/1, and templates.

def extra(app) do
  theme =
    case app.name.underscore do
      "rebel_" <> _ -> :rebel
      "empire_" <> _ -> :empire
      _ -> raise "App name must start with rebel_ or empire_ to derive theme."
    end

  [theme: theme]
end

This prevents you from having to redundantly set

[
  extra: [theme: :rebel]
]

In every uniform.exs manifest.

uniform.exs has precedence

If uniform.exs is [extra: [key: :manifest]], app.extra[:key] will (unsurprisingly) be :manifest in the extra/1 callback.

However, if extra/1 returns [key: :callback], app.extra[:key] will still be :manifest in modify/2, base_files/1, and templates.

In other words, uniform.exs "has precedence over" the extra/1 callback.

Link to this callback

target_path(path, app)

View Source (optional)
@callback target_path(path :: Path.t(), app :: Uniform.App.t()) :: Path.t()

Use this optional callback to change the path of files in the ejected codebase.

It will be called for every ejected file, with the exception that cp_r/2 will only call it once for the entire directory.

If you don't want to modify the path, just return it.

example

Example

You may want to place certain files in lib/ejected_app_web instead of lib/ejected_app. Let's say you have an is_web_file? function that identifies whether the file belongs in the _web directory. target_path might look like this:

def target_path(path, app) do
  if is_web_file?(path) do
    # modify the path to put it in `lib/some_app_web`
    String.replace(path, "lib/#{app.name.underscore}/", "lib/#{app.name.underscore}_web/")
  else
    path
  end
end

Link to this section Functions

Inside of a deps do block, any Mix or Lib dependencies that should be included in every ejected app should be wrapped in an always do block:

deps do
  always do
    # always eject the contents of `lib/some_lib`
    lib :some_lib

    # always eject the some_mix Mix dependency
    mix :some_mix
  end
end
Link to this macro

base_files(files)

View Source (macro)

The base_files section is where you specify files outside of an ejected app's lib/my_app and test/my_app directories which should always be ejected.

This section has access to an app variable which can be used to build the instructions or conditionally include certain files with if.

base_files do
  # interpolating the app name into a path dynamically
  cp "priv/static/images/#{app.name.underscore}_logo.png"

  # conditional instructions
  if deploys_to_fly_io?(app) do
    template "fly.toml"
  end
end

Note that if conditionals cannot be nested here.

api-reference

API Reference

example

Example

base_files do
  file ".credo.exs"
  file "config/**/*.exs"
  template "config/runtime.exs"
  cp "bin/some-executable", chmod: 0o555
  cp_r "assets"
end

files-in-lib

Files in lib

Typically, the base_files section only contains files that aren't in lib, since files in lib/app_being_ejected and lib/required_lib_dependency are ejected automatically.

# ❌ Don't do this
base_files do
  file "lib/my_lib/some_file.ex"
end

# ✅ Instead, do this (lib/my_app/uniform.exs)
[
  lib_deps: [:my_lib]
]

files-outside-lib-but-tied-to-lib-dependencies

Files outside lib but tied to Lib Dependencies

If a file or template should only be ejected in the case that a certain Lib Dependency is included, we recommend placing that inside the deps section instead of in base_files. (See Including files outside of lib.)

# ❌ Don't do this
base_files do
  if depends_on?(app, :lib, :my_lib) do
    file "some_file"
  end
end

# ✅ Instead, do this
deps do
  lib :my_lib do
    file "some_file"
  end
end

cp works exactly like file/2, except that no Code Transformations are applied to the file.

The file is copied as-is with File.cp!/3.

options

Options

cp takes a chmod option, which sets the mode for the file after it's copied. See the possible permission options.

examples

Examples

base_files do
  # every ejected app will have bin/some-binary, with the ACL mode changed to 555
  cp "bin/some-binary", chmod: 0o555
end

deps do
  lib :pdf do
    # apps that include the pdf lib will also have bin/convert
    cp "bin/convert"
  end
end

cp_r works like cp/2, but for a directory instead of a file. The directory is copied as-is with File.cp_r!/3.

None of the files are ran through Code Transformations.

This is useful for directories that do not require modification, and contain many files.

For example, the assets/node_modules directory in a Phoenix application would take ages to copy with file "assets/node_modules/**/*". Instead, use cp_r "assets/node_modules".

examples

Examples

base_files do
  cp_r "assets"
end

deps do
  lib :some_lib do
    cp_r "priv/files_for_some_lib"
  end
end
Link to this macro

deps(mix_and_lib_deps)

View Source (macro)
@spec deps(mix_and_lib_deps :: term()) :: term()

Uniform automatically catalogs all Mix deps by looking into mix.exs to discover all Mix deps. It also catalogs all Lib deps by scanning the lib/ directory.

If you need to configure anything about a Mix or Lib dep, such as other dependencies that must be bundled along with it, use the deps block.

See mix/2, lib/2, and always/1 for more details.

example

Example

deps do
  always do
    lib :my_app do
      only ["lib/my_app/application.ex"]
    end

    mix :phoenix
  end

  mix :absinthe do
    mix_deps [:absinthe_plug, :dataloader]
  end

  lib :my_custom_aws_lib do
    lib_deps [:my_utilities_lib]
    mix_deps [:ex_aws, :ex_aws_ec2]
  end
end

In the deps section of your Blueprint, you can specify that a Lib Dependency excludes certain files.

This works much like the except option that can be given when importing functions with import/2.

In the example below, for any app that depends on :aws, every file in lib/aws and test/aws will be ejected except for lib/aws/hidden_file.ex.

deps do
  lib :aws do
    except ["lib/aws/hidden_file.ex"]
  end
end

In base_files and lib blocks, file is used to specify files that are not in a lib/ directory which should be ejected in the app or along with the lib.

options

Options

  • chmod – Sets the mode for the given file after it's ejected. See the possible permission options.
  • match_dot – Forwarded to Path.wildcard/2. See "Wildcard Globs" below.

glob-expressions

Glob Expressions

You can use a glob expression with wildcard characters to target multiple files. (See "Examples" below.)

This is possible because Uniform internally forwards the path and opts to Path.wildcard/2 to determine which files to eject.

Note that the * and ? "wildcard characters" will not match files starting with a dot (.) unless you provide match_dot: true.

examples

Examples

base_files do
  file "assets/js/app.js"
  file "some/file", chmod: 0o777

  # glob targeting .exs files in config/
  file "config/**/*.exs"

  # glob targeting .exs files in priv/repo/ – including .formatter.exs
  file "priv/repo/**/*.exs", match_dot: true
end

deps do
  lib :aws do
    # for every app that includes the aws lib dependency,
    # some_aws_fixture.xml will also be included
    file "test/support/fixtures/some_aws_fixture.xml"
  end
end
Link to this macro

lib(name, list)

View Source (macro)

Lib Dependencies are shared code libraries in the lib directory of your project. Uniform scans lib/, so you don't need to inform it about them.

However, there are four scenarios where you do need to list them in your Blueprint.

1-specifying-which-lib-dependencies-should-always-be-ejected

1. Specifying which Lib Dependencies should always be ejected

deps do
  always do
    # every app will have lib/utilities
    lib :utilities
  end
end

See always/1.

2-when-a-lib-dependency-has-other-lib-or-mix-dependencies

2. When a Lib Dependency has other Lib or Mix Dependencies

deps do
  # if `lib/auth` is included, tesla and `lib/utils` will be included
  lib :auth do
    mix_deps [:tesla]
    lib_deps [:utils]
  end
end

See mix_deps/1 and lib_deps/1.

3-including-files-outside-of-lib

3. Including files outside of lib

Some libraries require other files outside of lib/that_library.

For example:

deps do
  # when `lib/my_data_source is included...
  lib :my_data_source do
    # files in priv/my_data_source_repo will be included
    file "priv/my_data_source_repo/**", match_dot: true

    # .ex files in `test/support/my_data_source` will be included
    file "test/support/my_data_source/**/*.ex"

    # `priv/my_data_source_file` will be included from a template
    template "priv/my_data_source_file"
  end
end

See file/2, template/2, cp/1, and cp_r/1.

4-when-certain-files-should-be-excluded-from-a-lib-dependency-upon-ejection

4. When certain files should be excluded from a Lib Dependency upon ejection

deps do
  always do
    # every app will have lib/mix, but only `some_task.ex` will be ejected
    lib :mix do
      only ["lib/mix/tasks/some_task.ex"]
    end
  end

  # `some_file.ex` will be omitted from `lib/auth` in ejected codebases
  lib :auth do
    except ["lib/auth/some_file.ex"]
  end
end

See only/1 and except/1.

Link to this macro

lib_deps(deps)

View Source (macro)

Specify transitive Lib Dependencies of other Lib Dependencies.

Libraries listed with lib_deps will be included in the ejected codebase any time the "parent" dependency is included.

examples

Examples

deps do
  # `lib/core_auth` will never be ejected without `lib/oauth_lib`
  lib :core_auth do
    lib_deps [:oauth_lib]
  end
end
Link to this macro

mix(name, list)

View Source (macro)

Uniform automatically catalogues the deps in mix.exs, so there are only two scenarios where you need to list them in your Blueprint.

1-specifying-deps-that-should-always-be-ejected

1. Specifying deps that should always be ejected.

deps do
  always do
    # every app will have credo and ex_doc
    mix :credo
    mix :ex_doc
  end
end

See always/1.

2-when-a-mix-dependency-has-other-mix-dependencies

2. When a Mix Dependency has other Mix Dependencies.

deps do
  mix :oban do
    # any app that is ejected with oban will also have oban_pro and oban_web
    mix_deps [:oban_pro, :oban_web]
  end
end

See mix_deps/1.

Link to this macro

mix_deps(deps)

View Source (macro)

Specify transitive Mix Dependencies of other Lib/Mix Dependencies.

Dependencies listed with mix_deps will be included in the ejected mix.exs any time the "parent" dependency is included.

examples

Examples

deps do
  # if absinthe is included, absinthe_plug and dataloader will be included
  mix :absinthe do
    mix_deps [:absinthe_plug, :dataloader]
  end

  # when `lib/ui_components` is included, surface will be included
  lib :ui_components do
    mix_deps [:surface]
  end
end
Link to this macro

modify(path_or_regex, function)

View Source (macro)
@spec modify(
  pattern :: Path.t() | Regex.t(),
  function ::
    (file :: String.t(), Uniform.App.t() -> String.t())
    | (file :: String.t() -> String.t())
) :: term()

Modify the contents of one or more files during ejection.

Takes a transformation function which returns the new file contents as a string.

The first argument is either the relative path of a file in your Base Project or a Regex.

# modify a specific test
modify "tests/path/to/specific_test.exs", fn file -> ... end

# modify all `_test.exs` files
modify ~r/_test.exs$/, fn file -> ... end

The second argument is either a function capture

modify ~r/.+_test.exs/, &MyApp.Modify.modify_tests/1
modify ~r/.+_test.exs/, &MyApp.Modify.modify_tests/2

Or an anonymous function

modify ~r/.+_test.exs/, fn file ->
  # ...
end

modify ~r/.+_test.exs/, fn file, app ->
  # ...
end

If the function is 1-arity, it will receive the file contents. If it's 2-arity, it will receive the file contents and the Uniform.App struct.

examples

Examples

modify "config/config.exs", file do
  file <>
    ~S'''
    if config_env() in [:dev, :test] do
      import_config "#{config_env()}.exs"
    end
    '''
end

modify ~r/.+_worker.ex/, fn file, app ->
  String.replace(file, "SOME_VALUE_PER_APP", app.extra[:some_value])
end

In the deps section of your Blueprint, you can specify that a Lib Dependency only includes certain files.

These work much like the only option that can be given when importing functions with import/2.

In the example below, for any app that depends on :gcp, only lib/gcp/necessary_file.ex will be ejected. Nothing else from lib/gcp or test/gcp will be ejected.

deps do
  lib :gcp do
    # NOTHING in lib/gcp or test/gcp will be included except these:
    only ["lib/gcp/necessary_file.ex"]
  end
end
Link to this function

template(path, opts \\ [])

View Source

In base_files and lib blocks, template is used to specify EEx templates that should be rendered and then ejected.

template-directory-and-destination-path

Template Directory and Destination Path

Uniform templates use a "convention over configuration" model that works like this:

  1. At the top of your Blueprint module, you specify a template directory like this:

    use Uniform, templates: "lib/uniform/templates"

  2. Templates must be placed in this directory at the relative path that they should be placed in, in the ejected directory.

  3. Templates must have the destination filename, appended with .eex.

Companion guide

Consult Building files from EEx templates for a more detailed look at constructing and effectively using templates for ejection.

options

Options

template takes a chmod option, which sets the mode for the rendered file after it's ejected. See the possible permission options.

examples

Examples

use Uniform, templates: "priv/uniform-templates"

base_files do
  # priv/uniform-templates/config/runtime.exs.eex will be rendered, and the
  # result will be placed in `config/runtime.exs` in every ejected app
  template "config/runtime.exs"
end

deps do
  lib :datadog do
    # for every app that includes `lib/datadog`,
    # priv/uniform-templates/datadog/prerun.sh.eex will be rendered, and
    # the result will be placed in `datadog/prerun.sh`
    template "datadog/prerun.sh", chmod: 0o555
  end
end