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*
"""
)
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{}, content) ->
Regex.replace(
~r{()},
content,
"\\1\n#{tag}",
global: false
)
Regex.match?(~r{(\s*
"""
)
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