defmodule PhoenixKit.Install.JsIntegration do @moduledoc """ Handles automatic JavaScript hooks integration for PhoenixKit installation. This module: - Copies `phoenix_kit.js` to the parent app's `priv/static/assets/vendor/` - Adds a ` """ ) end end defp inject_script_tag(igniter, layout_path, content) do script_tag = """ #{@script_marker} \ """ # Insert before the app.js script tag updated = String.replace( content, ~r{(\s*]*src=[^>]*app\.js[^>]*>)}, "\n#{script_tag}\n\\1", global: false ) if updated != content do File.write!(layout_path, updated) Igniter.add_notice( igniter, "✅ Added PhoenixKit JS hooks script tag to #{layout_path}" ) else Igniter.add_warning( igniter, """ ⚠️ Could not automatically add script tag to #{layout_path}. Please add this before your app.js script tag: #{@script_marker} """ ) end end defp add_hooks_to_app_js(igniter) do app_js_path = Path.join([File.cwd!(), "assets", "js", "app.js"]) if File.exists?(app_js_path) do content = File.read!(app_js_path) cond do # Match the actual spread, not any mention — a comment that merely names # PhoenixKitHooks must not skip the real injection. \s* handles the # multiline `hooks: {\n ...window.PhoenixKitHooks,` shape. Regex.match?(~r/\.\.\.\s*window\.PhoenixKitHooks\b/, content) -> # Already has the PhoenixKitHooks spread igniter Regex.match?(~r/hooks:\s*\{/, content) -> # Has a hooks object — inject PhoenixKitHooks spread at the start updated = Regex.replace( ~r/hooks:\s*\{/, content, "hooks: {...window.PhoenixKitHooks, ", global: false ) if updated != content do File.write!(app_js_path, updated) Igniter.add_notice( igniter, "✅ Added PhoenixKitHooks spread to LiveSocket hooks in assets/js/app.js" ) else add_hooks_manual_notice(igniter) end Regex.match?(~r/new LiveSocket\(/, content) -> # Has LiveSocket but no hooks — add hooks option updated = Regex.replace( ~r/(new LiveSocket\([^,]+,\s*Socket,\s*\{)/, content, "\\1\n hooks: {...window.PhoenixKitHooks},", global: false ) if updated != content do File.write!(app_js_path, updated) Igniter.add_notice( igniter, "✅ Added PhoenixKitHooks to LiveSocket configuration in assets/js/app.js" ) else add_hooks_manual_notice(igniter) end true -> add_hooks_manual_notice(igniter) end else add_hooks_manual_notice(igniter) end end defp add_hooks_manual_notice(igniter) do Igniter.add_warning( igniter, """ ⚠️ Could not automatically add PhoenixKitHooks to app.js. Please add ...window.PhoenixKitHooks to your LiveSocket hooks: const liveSocket = new LiveSocket("/live", Socket, { hooks: {...window.PhoenixKitHooks, ...yourOtherHooks}, }) """ ) end # Register the :phoenix_kit_js_sources compiler in the parent app's mix.exs. # It regenerates priv/static/assets/vendor/phoenix_kit_modules.js on each # compile from external modules' js_sources/0. defp add_js_sources_compiler(igniter) do Igniter.Project.MixProject.update(igniter, :project, [:compilers], fn nil -> # No :compilers key yet — must keep the defaults, so prepend to # Mix.compilers() rather than replacing the whole list. {:ok, {:code, quote(do: [:phoenix_kit_js_sources] ++ Mix.compilers())}} zipper -> case Igniter.Code.List.prepend_new_to_list(zipper, :phoenix_kit_js_sources) do {:ok, zipper} -> {:ok, zipper} :error -> {:warning, "Could not add :phoenix_kit_js_sources to compilers in mix.exs"} end end) rescue _ -> Igniter.add_warning( igniter, """ ⚠️ Could not add :phoenix_kit_js_sources compiler to mix.exs. Please add it manually: def project do [ ..., compilers: [:phoenix_kit_js_sources] ++ Mix.compilers() ] end """ ) end # Seed an empty aggregate file so the """ ) end end defp inject_modules_script_tag(igniter, layout_path, content) do tag = """ #{@modules_script_marker} \ """ # Prefer to anchor right after the phoenix_kit.js tag so ordering is explicit. # Escape the filename — its "." is a regex metachar, and a future vendor file # could otherwise false-match. pk = Regex.escape(@source_filename) updated = cond do Regex.match?(~r{]*#{pk}[^>]*>\s*}, content) -> Regex.replace( ~r{(]*#{pk}[^>]*>\s*)}, content, "\\1\n#{tag}", global: false ) Regex.match?(~r{(\s*]*src=[^>]*app\.js[^>]*>)}, content) -> Regex.replace( ~r{(\s*]*src=[^>]*app\.js[^>]*>)}, content, "\n#{tag}\n\\1", global: false ) true -> content end if updated != content do File.write!(layout_path, updated) Igniter.add_notice(igniter, "✅ Added module-hooks script tag to #{layout_path}") else Igniter.add_warning( igniter, """ ⚠️ Could not automatically add the module-hooks script tag to #{layout_path}. Please add this after phoenix_kit.js and before app.js: #{@modules_script_marker} """ ) end end defp resolve_source_path do # Try multiple possible locations candidates = [ # Path dependency (development) Path.join([File.cwd!(), "..", "phoenix_kit", "priv", "static", "assets", @source_filename]), # Hex dependency Path.join([:code.priv_dir(:phoenix_kit), "static", "assets", @source_filename]) ] case Enum.find(candidates, &File.exists?/1) do nil -> {:error, :not_found} path -> {:ok, path} end end defp dest_path do Path.join([File.cwd!(), "priv", "static", "assets", "vendor", @source_filename]) end defp find_existing_file(paths) do case Enum.find(paths, &File.exists?/1) do nil -> {:error, :not_found} path -> {:ok, path} end end end