diff --git a/lib/mix/tasks/phoenix_kit.install.ex b/lib/mix/tasks/phoenix_kit.install.ex index 6fc721264..1e941648a 100644 --- a/lib/mix/tasks/phoenix_kit.install.ex +++ b/lib/mix/tasks/phoenix_kit.install.ex @@ -49,6 +49,7 @@ if Code.ensure_loaded?(Igniter.Mix.Task) do alias PhoenixKit.Install.{ ApplicationSupervisor, AssetRebuild, + BasicConfiguration, BrowserPipelineIntegration, CssIntegration, DemoFiles, @@ -89,6 +90,7 @@ if Code.ensure_loaded?(Igniter.Mix.Task) do opts = igniter.args.options igniter + |> BasicConfiguration.add_basic_config() |> RepoDetection.add_phoenix_kit_configuration(opts[:repo]) |> MailerConfig.add_mailer_configuration() |> RateLimiterConfig.add_rate_limiter_configuration() @@ -192,7 +194,7 @@ if Code.ensure_loaded?(Igniter.Mix.Task) do # Install with custom PostgreSQL schema prefix for table isolation mix phoenix_kit.install --prefix "auth" --create-schema - + # Install with custom router path mix phoenix_kit.install --router-path lib/my_app_web/router.ex diff --git a/lib/mix/tasks/phoenix_kit.update.ex b/lib/mix/tasks/phoenix_kit.update.ex index fa44a7d7a..066f16f2f 100644 --- a/lib/mix/tasks/phoenix_kit.update.ex +++ b/lib/mix/tasks/phoenix_kit.update.ex @@ -70,6 +70,7 @@ if Code.ensure_loaded?(Igniter.Mix.Task) do alias PhoenixKit.Install.{ ApplicationSupervisor, AssetRebuild, + BasicConfiguration, Common, CssIntegration, RateLimiterConfig @@ -111,6 +112,7 @@ if Code.ensure_loaded?(Igniter.Mix.Task) do igniter else igniter + |> BasicConfiguration.add_basic_config() |> ApplicationSupervisor.add_supervisor() |> perform_igniter_update(opts) end diff --git a/lib/phoenix_kit/config.ex b/lib/phoenix_kit/config.ex index 8d23cfecf..e6661bc36 100644 --- a/lib/phoenix_kit/config.ex +++ b/lib/phoenix_kit/config.ex @@ -25,6 +25,8 @@ defmodule PhoenixKit.Config do """ @default_config [ + parent_app_name: nil, + parent_module: nil, repo: nil, mailer: nil, scheme: "http", @@ -204,16 +206,11 @@ defmodule PhoenixKit.Config do """ @spec get_parent_endpoint() :: {:ok, module()} | :error def get_parent_endpoint do - case get_parent_app() do - nil -> - :error - - app_name -> - base_module = app_name |> to_string() |> Macro.camelize() - + case get(:parent_module) do + {:ok, parent_module} -> potential_endpoints = [ - Module.concat([base_module <> "Web", "Endpoint"]), - Module.concat([base_module, "Endpoint"]) + Module.concat([String.to_atom("#{parent_module}Web"), Endpoint]), + Module.concat([parent_module, Endpoint]) ] Enum.reduce_while(potential_endpoints, :error, fn endpoint, _acc -> @@ -223,6 +220,9 @@ defmodule PhoenixKit.Config do {:cont, :error} end end) + + _ -> + :error end end @@ -246,30 +246,12 @@ defmodule PhoenixKit.Config do """ @spec get_parent_app() :: atom() | nil def get_parent_app do - # Get the application of the configured repo to determine parent app - case get(:repo) do - {:ok, repo_module} when is_atom(repo_module) -> - # Extract app name from repo module (e.g. MyApp.Repo -> :my_app) - repo_module - |> Module.split() - |> hd() - |> Macro.underscore() - |> String.to_atom() + case get(:parent_app_name) do + {:ok, app_name} -> + app_name _ -> - # Fallback: try to find the main application from the loaded applications - Application.loaded_applications() - |> Enum.find(fn {app, _, _} -> - app != :phoenix_kit and - app != :kernel and - app != :stdlib and - app != :elixir and - not String.starts_with?(to_string(app), "ex_") - end) - |> case do - {app, _, _} -> app - nil -> nil - end + get_parent_app_fallback() end end @@ -304,4 +286,90 @@ defmodule PhoenixKit.Config do :ok end + + # Fallback method to determine the parent application when explicit configuration is not available. + # + # This function implements a two-stage detection strategy: + # + # 1. **Primary Strategy**: Extract the application name from the configured repository module. + # For example, if `:repo` is configured as `MyApp.Repo`, this will return `:my_app`. + # + # 2. **Fallback Strategy**: Search through loaded applications to find the most likely + # parent application by filtering out system applications and dependencies. + # + # ## Detection Logic + # + # ### Repository-based Detection + # - Converts repository module names like `MyApp.Repo` to application atoms like `:my_app` + # - Uses Module.split() to break down the module name + # - Extracts the first segment and converts it to underscore format + # + # ### Application Search + # - Filters out system applications (`:kernel`, `:stdlib`, `:elixir`) + # - Excludes PhoenixKit itself (`:phoenix_kit`) + # - Excludes standard library applications (those starting with "ex_") + # - Returns the first remaining application, which is typically the parent app + # + # ## Examples + # + # # When repo is configured as MyApp.Repo + # # get_parent_app_fallback() -> :my_app + # + # # When no repo is configured, searches loaded applications + # # get_parent_app_fallback() -> :my_parent_app # First non-system application found + # + # # Returns nil if no suitable application is found + # # get_parent_app_fallback() -> nil + # + # ## Return Values + # + # - `atom()` - The detected parent application name + # - `nil` - No suitable parent application could be determined + # + # ## ⚠️ Reliability Warning + # + # **This function is not reliable and should not be depended upon for critical functionality.** + # + # The detection logic makes several assumptions that may not hold true in all environments: + # + # - Repository modules may not follow the `MyApp.Repo` convention + # - Application search may return incorrect results in complex dependency trees + # - Order of loaded applications is not guaranteed to be predictable + # - May return dependency applications instead of the actual parent application + # + # **For reliable behavior, always configure `:parent_app_name` explicitly** in your application + # configuration instead of relying on this fallback detection. + # + # ## Notes + # + # This function is used as a fallback when explicit `:parent_app_name` configuration + # is not provided. It enables PhoenixKit to automatically integrate with parent + # applications without requiring additional configuration in most cases. + defp get_parent_app_fallback() do + # Get the application of the configured repo to determine parent app + case get(:repo) do + {:ok, repo_module} when is_atom(repo_module) -> + # Extract app name from repo module (e.g. MyApp.Repo -> :my_app) + repo_module + |> Module.split() + |> hd() + |> Macro.underscore() + |> String.to_atom() + + _ -> + # Fallback: try to find the main application from the loaded applications + Application.loaded_applications() + |> Enum.find(fn {app, _, _} -> + app != :phoenix_kit and + app != :kernel and + app != :stdlib and + app != :elixir and + not String.starts_with?(to_string(app), "ex_") + end) + |> case do + {app, _, _} -> app + nil -> nil + end + end + end end diff --git a/lib/phoenix_kit/install/basic_configuration.ex b/lib/phoenix_kit/install/basic_configuration.ex new file mode 100644 index 000000000..d72c98fa7 --- /dev/null +++ b/lib/phoenix_kit/install/basic_configuration.ex @@ -0,0 +1,33 @@ +defmodule PhoenixKit.Install.BasicConfiguration do + @moduledoc """ + Installation helper for adding PhoenixKit supervisor to parent application. + Used by `mix phoenix_kit.install` task. + """ + alias Igniter.Project.Config + + alias PhoenixKit.Install.IgniterHelpers + + @doc """ + Adds basic PhoenixKit configuration to the parent application. + + Configures the parent app name and module in config.exs for PhoenixKit integration. + """ + def add_basic_config(igniter) do + parent_app_name = IgniterHelpers.get_parent_app_name(igniter) + parent_module = Igniter.Project.Module.module_name_prefix(igniter) + + igniter + |> Config.configure_new( + "config.exs", + :phoenix_kit, + :parent_app_name, + parent_app_name + ) + |> Config.configure_new( + "config.exs", + :phoenix_kit, + :parent_module, + parent_module + ) + end +end diff --git a/lib/phoenix_kit/install/repo_detection.ex b/lib/phoenix_kit/install/repo_detection.ex index 948348e8c..5a30f410e 100644 --- a/lib/phoenix_kit/install/repo_detection.ex +++ b/lib/phoenix_kit/install/repo_detection.ex @@ -10,6 +10,7 @@ defmodule PhoenixKit.Install.RepoDetection do use PhoenixKit.Install.IgniterCompat alias Igniter.Libs.Ecto + alias Igniter.Project.Config alias Igniter.Project.Module, as: IgniterModule alias PhoenixKit.Install.IgniterHelpers @@ -150,8 +151,6 @@ defmodule PhoenixKit.Install.RepoDetection do # Add repo configuration to config files defp add_repo_config_to_files(igniter, repo_module) do - alias Igniter.Project.Config - try do igniter # Add repo config to main config.exs diff --git a/lib/phoenix_kit/pages.ex b/lib/phoenix_kit/pages.ex index 3182b3427..169ba1265 100644 --- a/lib/phoenix_kit/pages.ex +++ b/lib/phoenix_kit/pages.ex @@ -5,6 +5,7 @@ defmodule PhoenixKit.Pages do Provides filesystem operations for creating, editing, and organizing files and folders in a web-based interface. """ + require Logger alias PhoenixKit.Config alias PhoenixKit.Pages.FileOperations @@ -133,10 +134,9 @@ defmodule PhoenixKit.Pages do "/path/to/app/priv/static/pages" """ def root_path do - parent_app = get_parent_app() + parent_app = Config.get_parent_app() path = resolve_pages_path(parent_app) - require Logger Logger.debug("Pages root_path: parent_app=#{inspect(parent_app)}, path=#{inspect(path)}") case File.mkdir_p(path) do @@ -147,23 +147,6 @@ defmodule PhoenixKit.Pages do # Private Helpers - defp get_parent_app do - case Config.get(:repo, nil) do - nil -> - # Fallback to phoenix_kit if no repo configured - :phoenix_kit - - repo_module -> - # Extract app name from repo module - # e.g., PhoenixKitTesting.Repo -> :phoenix_kit_testing - repo_module - |> Module.split() - |> List.first() - |> Macro.underscore() - |> String.to_atom() - end - end - defp resolve_pages_path(parent_app) do priv_dir = :code.priv_dir(parent_app) |> to_string()