defmodule Mix.Phoenix do # Conveniences for Phoenix tasks. @moduledoc false @doc """ Evals EEx files from source dir. Files are evaluated against EEx according to the given binding. """ def eval_from(apps, source_file_path, binding) do sources = Enum.map(apps, &to_app_source(&1, source_file_path)) content = Enum.find_value(sources, fn source -> File.exists?(source) && File.read!(source) end) || raise "could not find #{source_file_path} in any of the sources" EEx.eval_string(content, binding) end @doc """ Copies files from source dir to target dir according to the given map. Files are evaluated against EEx according to the given binding. """ def copy_from(apps, source_dir, binding, mapping) when is_list(mapping) do roots = Enum.map(apps, &to_app_source(&1, source_dir)) for {format, source_file_path, target} <- mapping do source = Enum.find_value(roots, fn root -> source = Path.join(root, source_file_path) if File.exists?(source), do: source end) || raise "could not find #{source_file_path} in any of the sources" case format do :text -> Mix.Generator.create_file(target, File.read!(source)) :eex -> Mix.Generator.create_file(target, EEx.eval_file(source, binding)) :new_eex -> if File.exists?(target) do :ok else Mix.Generator.create_file(target, EEx.eval_file(source, binding)) end end end end defp to_app_source(path, source_dir) when is_binary(path), do: Path.join(path, source_dir) defp to_app_source(app, source_dir) when is_atom(app), do: Application.app_dir(app, source_dir) @doc """ Inflects path, scope, alias and more from the given name. ## Examples iex> Mix.Phoenix.inflect("user") [alias: "User", human: "User", base: "Phoenix", web_module: "PhoenixWeb", module: "Phoenix.User", scoped: "User", singular: "user", path: "user"] iex> Mix.Phoenix.inflect("Admin.User") [alias: "User", human: "User", base: "Phoenix", web_module: "PhoenixWeb", module: "Phoenix.Admin.User", scoped: "Admin.User", singular: "user", path: "admin/user"] iex> Mix.Phoenix.inflect("Admin.SuperUser") [alias: "SuperUser", human: "Super user", base: "Phoenix", web_module: "PhoenixWeb", module: "Phoenix.Admin.SuperUser", scoped: "Admin.SuperUser", singular: "super_user", path: "admin/super_user"] """ def inflect(singular) do base = Mix.Phoenix.base() web_module = base |> web_module() |> inspect() scoped = Phoenix.Naming.camelize(singular) path = Phoenix.Naming.underscore(scoped) singular = String.split(path, "/") |> List.last module = Module.concat(base, scoped) |> inspect alias = String.split(module, ".") |> List.last human = Phoenix.Naming.humanize(singular) [alias: alias, human: human, base: base, web_module: web_module, module: module, scoped: scoped, singular: singular, path: path] end @doc """ Checks the availability of a given module name. """ def check_module_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 @doc """ Returns the module base name based on the configuration value. config :my_app namespace: My.App """ def base do app_base(otp_app()) end @doc """ Returns the context module base name based on the configuration value. config :my_app namespace: My.App """ def context_base(ctx_app) do app_base(ctx_app) end defp app_base(app) do case Application.get_env(app, :namespace, app) do ^app -> app |> to_string() |> Phoenix.Naming.camelize() mod -> mod |> inspect() end end @doc """ Returns the OTP app from the Mix project configuration. """ def otp_app do Mix.Project.config() |> Keyword.fetch!(:app) end @doc """ Returns all compiled modules in a project. """ def modules do Mix.Project.compile_path() |> Path.join("*.beam") |> Path.wildcard() |> Enum.map(&beam_to_module/1) end defp beam_to_module(path) do path |> Path.basename(".beam") |> String.to_atom() end @doc """ The paths to look for template files for generators. Defaults to checking the current app's `priv` directory, and falls back to Phoenix's `priv` directory. """ def generator_paths do [".", :phoenix] end @doc """ Checks if the given `app_path` is inside an umbrella. """ def in_umbrella?(app_path) do umbrella = Path.expand(Path.join [app_path, "..", ".."]) mix_path = Path.join(umbrella, "mix.exs") apps_path = Path.join(umbrella, "apps") File.exists?(mix_path) && File.exists?(apps_path) end @doc """ Returns the web prefix to be used in generated file specs. """ def web_path(ctx_app, rel_path \\ "") when is_atom(ctx_app) do this_app = otp_app() if ctx_app == this_app do Path.join(["lib", "#{this_app}_web", rel_path]) else Path.join(["lib", to_string(this_app), rel_path]) end end @doc """ Returns the context app path prefix to be used in generated context files. """ def context_app_path(ctx_app, rel_path) when is_atom(ctx_app) do this_app = otp_app() if ctx_app == this_app do rel_path else app_path = case Application.get_env(this_app, :generators)[:context_app] do {^ctx_app, path} -> Path.relative_to_cwd(path) _ -> mix_app_path(ctx_app, this_app) end Path.join(app_path, rel_path) end end @doc """ Returns the context lib path to be used in generated context files. """ def context_lib_path(ctx_app, rel_path) when is_atom(ctx_app) do context_app_path(ctx_app, Path.join(["lib", to_string(ctx_app), rel_path])) end @doc """ Returns the context test path to be used in generated context files. """ def context_test_path(ctx_app, rel_path) when is_atom(ctx_app) do context_app_path(ctx_app, Path.join(["test", to_string(ctx_app), rel_path])) end @doc """ Returns the OTP context app. """ def context_app do this_app = otp_app() case fetch_context_app(this_app) do {:ok, app} -> app :error -> this_app end end @doc """ Returns the test prefix to be used in generated file specs. """ def web_test_path(ctx_app, rel_path \\ "") when is_atom(ctx_app) do this_app = otp_app() if ctx_app == this_app do Path.join(["test", "#{this_app}_web", rel_path]) else Path.join(["test", to_string(this_app), rel_path]) end end defp fetch_context_app(this_otp_app) do case Application.get_env(this_otp_app, :generators)[:context_app] do nil -> :error false -> Mix.raise """ no context_app configured for current application #{this_otp_app}. Add the context_app generators config in config.exs, or pass the --context-app option explicitly to the generators. For example: via config: config :#{this_otp_app}, :generators, context_app: :some_app via cli option: mix phx.gen.[task] --context-app some_app Note: cli option only works when `context_app` is not set to `false` in the config. """ {app, _path} -> {:ok, app} app -> {:ok, app} end end defp mix_app_path(app, this_otp_app) do case Mix.Project.deps_paths() do %{^app => path} -> Path.relative_to_cwd(path) deps -> Mix.raise """ no directory for context_app #{inspect app} found in #{this_otp_app}'s deps. Ensure you have listed #{inspect app} as an in_umbrella dependency in mix.exs: def deps do [ {:#{app}, in_umbrella: true}, ... ] end Existing deps: #{inspect Map.keys(deps)} """ end end @doc """ Prompts to continue if any files exist. """ def prompt_for_conflicts(generator_files) do file_paths = Enum.flat_map(generator_files, fn {:new_eex, _, _path} -> [] {_kind, _, path} -> [path] end) case Enum.filter(file_paths, &File.exists?(&1)) do [] -> :ok conflicts -> Mix.shell().info""" The following files conflict with new files to be generated: #{Enum.map_join(conflicts, "\n", &" * #{&1}")} See the --web option to namespace similarly named resources """ unless Mix.shell().yes?("Proceed with interactive overwrite?") do System.halt() end end end @doc """ Returns the web module prefix. """ def web_module(base) do if base |> to_string() |> String.ends_with?("Web") do Module.concat([base]) else Module.concat(["#{base}Web"]) end end def to_text(data) do inspect data, limit: :infinity, printable_limit: :infinity end def prepend_newline(string) do "\n" <> string end end