View Source Setting up a Phoenix project
This guide walks through a typical process for setting up a Phoenix project. It assumes you've read the How It Works and Getting Started guides.
Imagine creating an entire new application simply by adding a Route and a single LiveView to an existing Elixir project.
This is in fact the vision that drove the creation of Uniform. The overhead of experimenting with new apps becomes extremely low, incentivizing the team to try out ideas without being inhibited by initial setup time.
setting-up-your-blueprint-module
Setting up your Blueprint module
Below is an example Blueprint module for ejecting the files common to Phoenix applications.
defmodule MyBaseApp.Uniform.Blueprint do
use Uniform.Blueprint, templates: "lib/my_base_app/uniform/templates"
base_files do
cp_r "assets"
cp_r "priv/static"
file "lib/my_base_app_web.ex"
file Path.wildcard("config/**/*.exs")
file Path.wildcard("test/support/*_case.ex")
end
deps do
# Always eject the `my_base_app` and `my_base_app_web` libraries.
#
# Their paths and file contents will replace my_base_app with the ejected
# app name automatically.
always do
lib :my_base_app do
only [
"lib/my_base_app/application.ex",
"lib/my_base_app/repo.ex"
]
# `match_dot: true` so that we eject `priv/repo/.formatter.exs`
file Path.wildcard("priv/repo/**/*.exs", match_dot: true)
end
lib :my_base_app_web do
only [
"lib/my_base_app_web/endpoint.ex",
"lib/my_base_app_web/gettext.ex",
"lib/my_base_app_web/router.ex",
"lib/my_base_app_web/telemetry.ex",
"lib/my_base_app_web/channels/user_socket.ex",
"lib/my_base_app_web/templates/layout/app.html.heex",
"lib/my_base_app_web/templates/layout/live.html.heex"
"lib/my_base_app_web/templates/layout/root.html.heex",
"lib/my_base_app_web/views/error_helpers.ex",
"lib/my_base_app_web/views/error_view.ex",
"lib/my_base_app_web/views/layout_view.ex"
]
end
end
end
end
Let's walk through it step by step.
the-base_files-section
The base_files
section
base_files do
cp_r "assets"
cp_r "priv/static"
file "lib/my_base_app_web.ex"
file Path.wildcard("config/**/*.exs")
file Path.wildcard("test/support/*_case.ex")
end
In the base_files section, we specify
files that should always be ejected in every app. Phoenix apps will typically
have CSS and JS assets in the assets
directory. They'll also have static
files to be served as-is in priv/static
. Some of these files are binary
(non-text) files, and we assume none of them need to pass through the Code
Transformation phase. That's why the first two
lines are included.
cp_r "assets"
cp_r "priv/static"
Note that cp_r
instructs mix uniform.eject
to copy all the directory
contents (using File.cp_r!/3
).
Phoenix apps typically have a Web
module which is used to construct
Controllers, Views, Routers, and LiveViews. Since this file is typically in
lib/
directly (and not in a sub-directory of lib/
), we include it here in
the base_files
section.
file "lib/my_base_app_web.ex"
We also proceed with the assumption that the ejected app will need the Base Project's configuration files.
file Path.wildcard("config/**/*.exs")
the-deps-section
The deps
section
In the deps
section, we put both lib :my_base_app
and lib :my_base_app_web
inside always do
so that their contents are always ejected
without having to specify lib_deps: [:my_base_app, :my_base_app_web]
in the
uniform.exs
manifest of every app.
deps do
always do
lib :my_base_app do
# ...
end
lib :my_base_app_web do
# ...
end
end
end
For :my_base_app
, we use an only
instruction to exclude all files in
lib/my_base_app
and test/my_base_app
except for application.ex
and
repo.ex
.
lib :my_base_app do
only [
"lib/my_base_app/application.ex",
"lib/my_base_app/repo.ex"
]
file Path.wildcard("priv/repo/**/*.exs", match_dot: true)
end
You may have a setup that requires you to add more files, such as mailer.ex
.
Note that we also include all of the Repo's migrations and seeds scripts with
file Path.wildcard(...)
. The match_dot: true
ensures
priv/repo/.formatter.exs
is ejected so that the ejected codebase is formatted
properly.
For :my_base_app_web
, we also use an only
instruction to only include
relevant files.
lib :my_base_app_web do
only [
"lib/my_base_app_web/endpoint.ex",
"lib/my_base_app_web/gettext.ex",
"lib/my_base_app_web/router.ex",
"lib/my_base_app_web/telemetry.ex",
"lib/my_base_app_web/channels/user_socket.ex",
"lib/my_base_app_web/templates/layout/app.html.heex",
"lib/my_base_app_web/templates/layout/live.html.heex"
"lib/my_base_app_web/templates/layout/root.html.heex",
"lib/my_base_app_web/views/error_helpers.ex",
"lib/my_base_app_web/views/error_view.ex",
"lib/my_base_app_web/views/layout_view.ex"
]
end
the-phoenix-router
The Phoenix Router
A simple way to set up your Phoenix.Router
is to use Code
Fences and
Uniform.Blueprint.modify/2
.
Routes for all of your apps are all placed in the router, with two caveats:
- Routes are wrapped in Code Fences so that they're removed when other apps are ejected.
- Paths are prefixed with
/app-name
so that each app exists at a nice URL for development. The prefixes are removed withmodify
.
defmodule MyBaseAppWeb.Router do
use MyBaseAppWeb, :router
pipeline :browser do
# ...
end
# uniform:remove
# Place "internal pages" that should never be ejected here.
# See "Internal Pages" below this code example.
scope "/", SomeAppWeb do
pipe_through :browser
get "/internal-team-page", InternalTeamController, :index
end
# /uniform:remove
# uniform:app:some_app
scope "/some-app", SomeAppWeb do
pipe_through :browser
get "/widgets", WidgetController, :index
get "/widgets/new", WidgetController, :new
post "/widgets/new", WidgetController, :create
get "/widgets/:widget_id", WidgetController, :show
end
# /uniform:app:some_app
# uniform:app:another_app
scope "/another-app", SomeAppWeb do
pipe_through :browser
get "/widgets", WidgetController, :index
get "/widgets/new", WidgetController, :new
post "/widgets/new", WidgetController, :create
get "/widgets/:widget_id", WidgetController, :show
end
# /uniform:app:another_app
end
defmodule MyBaseApp.Uniform.Blueprint do
use Uniform.Blueprint, templates: "..."
modify "lib/my_base_app_web/router.ex", fn file, app ->
String.replace(
file,
"scope \"/#{app.name.hyphen}\"",
"scope \"/\""
)
end
end
The Code Fences (comments like this: # uniform:app:some_app
) will cause the
code to be removed when ejecting a different app. The simple code transformer
defined with modify
changes
scope "/some-app", SomeAppWeb do
To
scope "/", SomeAppWeb do
In the ejected codebase.
This method is a great starting point. Before you reach dozens of apps, you may want to consider other methods that allow you to define routes in a separate file per app.
internal-pages
Internal Pages
We encourage running all apps simultaneously via the Base Project as your development environment. In such a setup, it can be useful to add other pages to the Base Project that aren't intended to be ejected with any app.
For example, you might add a page that catalogs and links to your various apps.
We recommend adding these routes and wrapping them all in # uniform:remove
Code Fences as in the example above.
code-fences-everywhere
Code Fences Everywhere!
There are other files which are central for running Elixir apps.
Similarly to the Phoenix Router, we recommend that you add the code required by each of your apps and Lib Dependencies to all of these files. Then, use Code Fences to selectively remove code during ejection.
Let's examine what this might look like for application.ex
, mix.exs
, and
config/*.exs
files.
application
Application
Your Application
file at lib/my_base_app/application.ex
is a critical piece
of Elixir applications since it's used to start processes and supervisors at
the start of the application.
Here's what an example Application
file would look like with Code Fences
applied.
defmodule MyBaseApp.Application do
use Application
def start(_type, _args) do
children = [
MyBaseAppWeb.Endpoint,
{Phoenix.PubSub, name: MyBaseApp.PubSub},
MyBaseAppWeb.Presence,
MyBaseAppWeb.Telemetry,
# uniform:lib:my_first_data_lib
MyFirstDataLib.Repo,
# /uniform:lib:my_first_data_lib
# uniform:lib:my_second_data_lib
MySecondDataLib.Repo,
MySecondDataLib.Vault,
# /uniform:lib:my_second_data_lib
# uniform:remove
SomeDevelopmentOnlyDB.Repo,
# /uniform:remove
# uniform:mix:oban
{Oban, ...},
# /uniform:mix:oban
]
# ...
end
end
Notice that code which should always be ejected does not get surrounded by Code Fences.
mix-exs
mix.exs
Some dependencies require modifying mix.exs
. For example, the
exq Hex package says to add :exq
to application
in mix.exs
.
def application do
[
applications: [:logger, :exq],
# ...
]
end
But what if only some of your apps require exq? Wrap the exq-specific code in Code Fences, and it will only be included when exq is required.
def application do
[
applications: [
:logger,
# uniform:mix:exq
:exq
# /uniform:mix:exq
],
# ...
]
end
You don't need to use Code Fences in deps
Note that removing deps from the deps
section of mix.exs
is automatic if
you follow the Getting Started instructions properly.
So this would not be required.
# ❌ Do NOT wrap individual deps in code fences
defp deps do
[
# uniform:mix:jason
{:jason, "~> 1.0"},
# /uniform:mix:jason
...
]
end
Instead, wrap your entire deps list in uniform:deps
fences.
# ✅ Do use `uniform:deps` fences
defp deps do
# uniform:deps
[
{:jason, "~> 1.0"},
...
]
# /uniform:deps
end
config-files
Config Files
Many dependencies also require configuration. Apply code fences in your configuration files for the same result.
# uniform:mix:guardian
config :my_base_app, MyBaseApp.Guardian,
issuer: "my_base_app",
secret_key: ...
# /uniform:mix:guardian