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 >. <% 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