Current section

Files

Jump to
uniform lib uniform.ex
Raw

lib/uniform.ex

defmodule Uniform do
@moduledoc """
> Write less boilerplate and reuse more code in your portfolio of Elixir apps
With Uniform, developers maintain multiple Elixir apps inside a Base Project:
a "monolith" containing every app. Before deployment, the apps are "ejected"
into separate codebases containing only the code needed by each app.
The entire process is automated, so there's much less work required to start
a new app or share capabilities between apps.
## Recommended Guides
In order to understand and use this library, we heavily recommend reading the
following guides:
- [How It Works](how-it-works.html)
- [Dependencies](dependencies.html)
- [Code Transformations](code-transformations.html)
The [Setting up a Phoenix project](setting-up-a-phoenix-project.html) guide
is recommended if you're building Phoenix apps.
## Usage
```bash
mix uniform.eject tweeter
```
## Installation
Consult the [Getting Started](getting-started.html) guide to add Uniform to
an Elixir application.
In summary, you'll need to:
1. Add the dep in `mix.exs`: `{:uniform, "~> 0.1.0"}`
2. Add a [Blueprint](Uniform.Blueprint.html) module to your project
3. Configure your Elixir app to point to the Blueprint module
4. Add `uniform.exs` manifests to each Ejectable Application
5. Add to the Blueprint module all the files necessary to eject a working
application
"""
require Logger
@typep prepare_opt :: {:destination, String.t()}
@doc """
Returns a list of all [Ejectable App](how-it-works.html#ejectable-apps) names
in your Base Project.
### Examples
```bash
$ fd uniform.exs
lib/tweeter/uniform.exs
lib/trillo/uniform.exs
lib/hatmail/uniform.exs
```
iex> ejectable_app_names()
["tweeter", "trillo", "hatmail"]
"""
@spec ejectable_app_names :: [String.t()]
def ejectable_app_names do
"lib/*/uniform.exs"
|> Path.wildcard()
|> Enum.map(&(&1 |> Path.dirname() |> Path.basename()))
|> Enum.sort()
end
@doc """
Return a list of all [Ejectable Apps](how-it-works.html#ejectable-apps) in
your Base Project as `Uniform.App` structs.
### Example
```bash
$ fd uniform.exs
lib/tweeter/uniform.exs
lib/trillo/uniform.exs
lib/hatmail/uniform.exs
```
iex> ejectable_apps()
[
#Uniform.App<
extra: [...],
name: %{camel: "Tweeter", hyphen: "tweeter", module: Tweeter, underscore: "tweeter"},
...
>,
#Uniform.App<
extra: [...],
name: %{camel: "Trillo", hyphen: "trillo", module: Trillo, underscore: "trillo"},
...
>,
#Uniform.App<
extra: [...],
name: %{camel: "Hatmail", hyphen: "hatmail", module: Hatmail, underscore: "hatmail"},
...
>
]
"""
@spec ejectable_apps :: [Uniform.App.t()]
def ejectable_apps do
for name <- ejectable_app_names() do
prepare(%{name: name, opts: []})
end
end
@doc """
Prepares the `t:Uniform.App.t/0` struct with all information needed for ejection.
When ejecting an app, this step runs prior to the actual `eject/1` process,
allowing the user to see pertinent information about what decisions will be made
during ejection: (e.g. which dependencies will be included, where on
disk the ejected app will be written, etc.). If there is a mistake, the user will
have a chance to abort before performing a potentially destructive action.
""" && false
@spec prepare(init :: %{name: atom, opts: [prepare_opt]}) :: Uniform.App.t()
def prepare(%{name: name, opts: opts}) do
# ensure the name was passed in under_score format; otherwise subtle bugs happen
unless name in Uniform.ejectable_app_names() do
raise ArgumentError,
message: """
The name must correspond to a directory in lib, in under_score format.
For example, to eject `lib/my_app`, do:
mix uniform.eject my_app
"""
end
Mix.Task.run("compile", [])
config = Uniform.Config.build()
manifest = Uniform.Manifest.eval_and_parse(config, Macro.underscore(name))
Uniform.App.new!(config, manifest, name, opts)
end
@doc """
Ejects an app. That is, deletes the files in the destination and copies a fresh
set of files for that app.
""" && false
def eject(app) do
clear_destination(app)
Logger.info("📂 #{app.destination}")
File.mkdir_p!(app.destination)
for ejectable <- Uniform.File.all_for_app(app) do
Logger.info("💾 [#{ejectable.type}] #{ejectable.destination}")
Uniform.File.eject!(ejectable, app)
end
# remove mix deps that are not needed for this project from mix.lock
System.cmd("mix", ["deps.clean", "--unlock", "--unused"], cd: app.destination)
System.cmd("mix", ["format"], cd: app.destination)
end
@doc "Clear the destination folder where the app will be ejected." && false
def clear_destination(app) do
if File.exists?(app.destination) do
{:module, _} = Code.ensure_loaded(app.internal.config.blueprint)
preserve = app.internal.config.blueprint.__preserve__()
preserve = [".git", "deps", "_build" | preserve]
app.destination
|> File.ls!()
|> Enum.reject(&(&1 in preserve))
|> Enum.each(fn file_or_folder ->
path = Path.join(app.destination, file_or_folder)
Logger.info("💥 #{path}")
File.rm_rf(path)
end)
end
end
end