Packages

Generators for Elixir apps using Dotenvy for config including variants of the phx_new generators

Current section

Files

Jump to
dotenvy_generators lib mix tasks dot.new.ex
Raw

lib/mix/tasks/dot.new.ex

defmodule Mix.Tasks.Dot.New do
use Mix.Task
import DotenvyGenerators.Generator
@shortdoc "Creates a new Elixir project using Dotenvy for configuration"
@moduledoc """
Creates a new Elixir project using Dotenvy for configuration.
It expects the path of the project as argument.
mix new PATH [--app APP] [--module MODULE] [--sup] [--umbrella]
A project at the given PATH will be created. The
application name and module name will be retrieved
from the path, unless `--module` or `--app` is given.
An `--app` option can be given in order to
name the OTP application for the project.
A `--module` option can be given in order
to name the modules in the generated code skeleton.
A `--sup` option can be given to generate an OTP application
skeleton including a supervision tree. Normally an app is
generated without a supervisor and without the app callback.
An `--umbrella` option can be given to generate an
umbrella project.
## Examples
mix dot.new hello_world
Is equivalent to:
mix dot.new hello_world --module HelloWorld
To generate an app with a supervision tree and an application callback:
mix dot.new hello_world --sup
To generate an umbrella application with sub applications:
mix dot.new hello_world --umbrella
cd hello_world/apps
mix new child_app
"""
@switches [
app: :string,
module: :string,
sup: :boolean,
umbrella: :boolean
]
@impl true
def run(argv) do
{opts, argv} = OptionParser.parse!(argv, strict: @switches)
case argv do
[] ->
Mix.raise("Expected PATH to be given, please use \"mix new PATH\"")
[path | _] ->
app = opts[:app] || Path.basename(Path.expand(path))
check_application_name!(app, !opts[:app])
mod = opts[:module] || Macro.camelize(app)
check_mod_name_validity!(mod)
check_mod_name_availability!(mod)
unless path == "." do
check_directory_existence!(path)
File.mkdir_p!(path)
end
File.cd!(path, fn ->
if opts[:umbrella] do
generate_umbrella(app, mod, path, opts)
else
generate(app, mod, path, opts)
end
end)
end
end
defp generate(app, mod, path, opts) do
assigns = [
app: app,
mod: mod,
sup_app: sup_app(mod, !!opts[:sup]),
version: get_version(System.version())
]
mod_filename = Macro.underscore(mod)
create_file("README.md", readme_template(assigns))
create_file(".formatter.exs", formatter_template(assigns))
create_file(".gitignore", gitignore_template(assigns))
if in_umbrella?() do
create_file("mix.exs", mix_exs_apps_template(assigns))
else
create_file("mix.exs", mix_exs_template(assigns))
end
create_directory("lib")
create_file("lib/#{mod_filename}.ex", lib_template(assigns))
if opts[:sup] do
create_file("lib/#{mod_filename}/application.ex", lib_app_template(assigns))
end
create_directory("config")
create_file("config/config.exs", parent_config_template(assigns))
create_file("config/dev.exs", env_config_template([{:env, :dev} | assigns]))
create_file("config/test.exs", env_config_template([{:env, :test} | assigns]))
create_file("config/prod.exs", env_config_template([{:env, :prod} | assigns]))
create_file("config/runtime.exs", runtime_config_template(assigns))
create_directory("envs")
create_file("envs/.env", env_template([{:env, "default"} | assigns]))
create_file("envs/.dev.env", env_template([{:env, :dev} | assigns]))
create_file("envs/.test.env", env_template([{:env, :test} | assigns]))
create_file("envs/.prod.env", env_template([{:env, :prod} | assigns]))
create_directory("test")
create_file("test/test_helper.exs", test_helper_template(assigns))
create_file("test/#{mod_filename}_test.exs", test_template(assigns))
"""
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
#{cd_path(path)}mix deps.get
mix test
Run "mix help" for more commands.
"""
|> String.trim_trailing()
|> Mix.shell().info()
end
@doc """
Returns a list of reserved application names.
"""
def reserved_application_names do
# 1. Command line flags with multiple args can conflict with application names
# 2. OTP names
# 3. Elixir names
~w(boot_var compile config configfd env pa pz path run s setcookie)a ++
~w(otp asn1 common_test compiler crypto debugger dialyzer diameter
edoc eldap erl_docgen erl_interface erts et eunit ftp hipe
inets jinterface kernel megaco mnesia observer odbc os_mon
parsetools public_key reltool runtime_tools sasl snmp ssh
ssl stdlib syntax_tools toolbar tools typer wx xmerl)a ++
~w(eex elixir ex_unit iex logger mix)a
end
defp sup_app(_mod, false), do: ""
defp sup_app(mod, true), do: ",\n mod: {#{mod}.Application, []}"
defp cd_path("."), do: ""
defp cd_path(path), do: "cd #{path}\n "
defp generate_umbrella(_app, mod, path, _opts) do
assigns = [app: nil, mod: mod]
create_file("README.md", readme_template(assigns))
create_file(".formatter.exs", formatter_umbrella_template(assigns))
create_file(".gitignore", gitignore_template(assigns))
create_file("mix.exs", mix_exs_umbrella_template(assigns))
create_directory("apps")
create_directory("config")
create_file("config/config.exs", config_umbrella_template(assigns))
"""
Your umbrella project was created successfully.
Inside your project, you will find an apps/ directory
where you can create and host many apps:
#{cd_path(path)}cd apps
mix new my_app
Commands like "mix compile" and "mix test" when executed
in the umbrella project root will automatically run
for each application in the apps/ directory.
"""
|> String.trim_trailing()
|> Mix.shell().info()
end
defp check_application_name!(name, inferred?) do
if message = invalid_app(name) || reserved_app(name) do
Mix.raise(
message <>
if inferred? do
". The application name is inferred from the path, if you'd like to " <>
"explicitly name the application then use the \"--app APP\" option"
else
""
end
)
end
end
defp invalid_app(name) do
unless name =~ ~r/^[a-z][a-z0-9_]*$/ do
"Application name must start with a lowercase ASCII letter, followed by " <>
"lowercase ASCII letters, numbers, or underscores, got: #{inspect(name)}"
end
end
defp reserved_app(name) do
atom_name = String.to_atom(name)
if atom_name in reserved_application_names() or Application.ensure_loaded(atom_name) == :ok do
"Cannot use application name #{inspect(name)} because it is already used by Erlang/OTP or Elixir"
end
end
defp check_mod_name_validity!(name) do
unless name =~ ~r/^[A-Z]\w*(\.[A-Z]\w*)*$/ do
Mix.raise(
"Module name must be a valid Elixir alias (for example: Foo.Bar), got: #{inspect(name)}"
)
end
end
defp check_mod_name_availability!(name) do
name = Module.concat(Elixir, name)
if Code.ensure_loaded?(name) do
Mix.raise("Module name #{inspect(name)} is already taken, please choose another name")
end
end
defp check_directory_existence!(path) do
msg = "The directory #{inspect(path)} already exists. Are you sure you want to continue?"
if File.dir?(path) and not Mix.shell().yes?(msg) do
Mix.raise("Please select another directory for installation")
end
end
defp get_version(version) do
{:ok, version} = Version.parse(version)
"#{version.major}.#{version.minor}" <>
case version.pre do
[h | _] -> "-#{h}"
[] -> ""
end
end
defp in_umbrella? do
apps = Path.dirname(File.cwd!())
try do
Mix.Project.in_project(:umbrella_check, "../..", fn _ ->
path = Mix.Project.config()[:apps_path]
path && Path.expand(path) == apps
end)
catch
_, _ -> false
end
end
embed_template(:readme, """
# <%= @mod %>
**TODO: Add description**
<%= if @app do %>
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `<%= @app %>` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:<%= @app %>, "~> 0.1.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/<%= @app %>>.
<% end %>
""")
embed_template(:formatter, """
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
""")
embed_template(:formatter_umbrella, """
# Used by "mix format"
[
inputs: ["mix.exs", "config/*.exs"],
subdirectories: ["apps/*"]
]
""")
embed_template(:gitignore, """
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where third-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
<%= if @app do %>
# Ignore package tarball (built via "mix hex.build").
<%= @app %>-*.tar
<% end %>
# Temporary files, for example, from tests.
/tmp/
# Ignore custom overrides of .env files
.overrides.env
""")
embed_template(:mix_exs, """
defmodule <%= @mod %>.MixProject do
use Mix.Project
def project do
[
app: :<%= @app %>,
version: "0.1.0",
elixir: "~> <%= @version %>",
start_permanent: Mix.env() == :prod,
deps: deps(),
releases: releases()
]
end
defp releases do
[
<%= @app %>: [
overlays: ["envs/"]
]
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]<%= @sup_app %>
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:dotenvy, "~> 0.9.0"}
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end
""")
embed_template(:mix_exs_apps, """
defmodule <%= @mod %>.MixProject do
use Mix.Project
def project do
[
app: :<%= @app %>,
version: "0.1.0",
build_path: "../../_build",
config_path: "../../config/config.exs",
deps_path: "../../deps",
lockfile: "../../mix.lock",
elixir: "~> <%= @version %>",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]<%= @sup_app %>
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"},
# {:sibling_app_in_umbrella, in_umbrella: true}
]
end
end
""")
embed_template(:mix_exs_umbrella, """
defmodule <%= @mod %>.MixProject do
use Mix.Project
def project do
[
apps_path: "apps",
version: "0.1.0",
start_permanent: Mix.env() == :prod,
deps: deps(),
releases: releases()
]
end
defp releases do
[
<%= @app %>: [
overlays: ["envs/"]
]
]
end
# Dependencies listed here are available only for this
# project and cannot be accessed from applications inside
# the apps folder.
#
# Run "mix help deps" for examples and options.
defp deps do
[
{:dotenvy, "~> 0.9.0"}
]
end
end
""")
embed_template(:config_umbrella, ~S"""
# This file is responsible for configuring your umbrella
# and **all applications** and their dependencies with the
# help of the Config module.
#
# Note that all applications in your umbrella share the
# same configuration and dependencies, which is why they
# all use the same configuration file. If you want different
# configurations or dependencies per app, it is best to
# move said applications out of the umbrella.
import Config
# Sample configuration:
#
# config :logger, :console,
# level: :info,
# format: "$date $time [$level] $metadata$message\n",
# metadata: [:user_id]
#
""")
embed_template(:parent_config, ~S"""
# This file is responsible for configuring your application
# and its dependencies with the aid of the Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
# General compile-time application configuration
import Config
# Sample configuration:
#
# config :logger, :console,
# level: :info,
# format: "$date $time [$level] $metadata$message\n",
# metadata: [:user_id]
#
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
""")
embed_template(:env_config, ~S"""
# Compile-time configuration for @env environment
import Config
""")
embed_template(:runtime_config, ~S"""
# Runtime configuration
import Config
import Dotenvy
# For local development, read dotenv files inside the envs/ dir;
# for releases, read them at the RELEASE_ROOT
env_dir_prefix = System.get_env("RELEASE_ROOT") || Path.expand("./envs/") <> "/"
source!([
"#{env_dir_prefix}.env",
"#{env_dir_prefix}.#{config_env()}.env",
"#{env_dir_prefix}.#{config_env()}.overrides.env",
System.get_env()
])
# Sample configuration read from .envs inside envs/
config :<%= @app %>, :secret, env!("SECRET", :string!)
""")
embed_template(:env, ~S"""
SECRET=my-secret-<%= @env %>
""")
embed_template(:lib, """
defmodule <%= @mod %> do
@moduledoc \"""
Documentation for `<%= @mod %>`.
\"""
@doc \"""
Hello world.
## Examples
iex> <%= @mod %>.hello()
:world
\"""
def hello do
:world
end
end
""")
embed_template(:lib_app, """
defmodule <%= @mod %>.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
# Starts a worker by calling: <%= @mod %>.Worker.start_link(arg)
# {<%= @mod %>.Worker, arg}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: <%= @mod %>.Supervisor]
Supervisor.start_link(children, opts)
end
end
""")
embed_template(:test, """
defmodule <%= @mod %>Test do
use ExUnit.Case
doctest <%= @mod %>
test "greets the world" do
assert <%= @mod %>.hello() == :world
end
end
""")
embed_template(:test_helper, """
ExUnit.start()
""")
end