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
@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 theextra/1
callback.However, if
extra/1
returns[key: :callback]
,app.extra[:key]
will still be:manifest
inmodify/2
,base_files/1
, and templates.In other words,
uniform.exs
"has precedence over" theextra/1
callback.
@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
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
file/2
ejects a single file or list of filestemplate/2
creates a new file on ejection from an EEx templatecp/2
copies a file (likefile/2
) without running it through Code Transformations. This is useful for binary files such as images or executable.cp_r/2
copies an entire directory of files without Code Transformations.
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
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 themode
for the givenfile
after it's ejected. See the possible permission options.match_dot
– Forwarded toPath.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
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
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
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
.
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
@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
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:
At the top of your
Blueprint
module, you specify a template directory like this:use Uniform, templates: "lib/uniform/templates"
Templates must be placed in this directory at the relative path that they should be placed in, in the ejected directory.
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