Packages

phoenix_kit

1.7.156
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit install js_integration.ex
Raw

lib/phoenix_kit/install/js_integration.ex

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 `<script>` tag to the root layout so hooks are loaded before LiveSocket
The JS file defines `window.PhoenixKitHooks` which is spread into LiveSocket's
hooks object in app.js: `hooks: { ...window.PhoenixKitHooks, ...colocatedHooks }`
## External module hooks
External modules declare prebuilt hook bundles via `js_sources/0`. The
`:phoenix_kit_js_sources` compiler concatenates those into
`priv/static/assets/vendor/phoenix_kit_modules.js` on every compile and folds
their hooks into `window.PhoenixKitHooks`. This module:
- registers that compiler in the parent app's `mix.exs`,
- adds a single (stable) `<script>` tag for the aggregate file after
`phoenix_kit.js` and before `app.js`,
- seeds an empty aggregate file so the tag doesn't 404 before the first compile.
The tag never changes as modules come and go — only the file's content does — so
module JS stays zero-config, exactly like `css_sources/0`.
"""
require Logger
@source_filename "phoenix_kit.js"
@script_marker "<!-- PhoenixKit JS Hooks -->"
@modules_filename "phoenix_kit_modules.js"
@modules_script_marker "<!-- PhoenixKit Module Hooks -->"
@doc """
Copies phoenix_kit.js to the parent app's static vendor directory and
adds a script tag to the root layout.
Safe to run multiple times (idempotent).
"""
def add_js_integration(igniter) do
igniter
|> copy_js_file()
|> add_script_tag_to_layout()
|> add_hooks_to_app_js()
|> ensure_module_js_integration()
end
@doc """
Ensures the external-module JS integration is wired: the
`:phoenix_kit_js_sources` compiler is in `mix.exs`, an aggregate file is seeded,
and the root layout has the (stable) module-hooks `<script>` tag.
Idempotent. Called at install and at `mix phoenix_kit.update`, so hosts that
installed before this feature pick it up on their next update.
"""
def ensure_module_js_integration(igniter) do
igniter
|> add_js_sources_compiler()
|> seed_modules_js_file()
|> add_modules_script_tag_to_layout()
end
@doc """
Updates the JS file in the parent app's static vendor directory.
Called during `mix phoenix_kit.update` to keep hooks in sync.
"""
def update_js_file do
case resolve_source_path() do
{:ok, source} ->
dest = dest_path()
File.mkdir_p(Path.dirname(dest))
case File.cp(source, dest) do
:ok ->
Logger.info("Updated #{@source_filename} in priv/static/assets/vendor/")
:ok
{:error, reason} ->
Logger.warning("Failed to update #{@source_filename}: #{inspect(reason)}")
{:error, reason}
end
{:error, :not_found} ->
Logger.warning("Could not find #{@source_filename} in PhoenixKit priv/static/assets/")
{:error, :not_found}
end
rescue
error ->
Logger.warning("Unexpected error updating #{@source_filename}: #{inspect(error)}")
{:error, :unexpected}
end
# ── Private ──────────────────────────────────────────────────────
defp copy_js_file(igniter) do
case resolve_source_path() do
{:ok, source} ->
dest = dest_path()
File.mkdir_p(Path.dirname(dest))
case File.cp(source, dest) do
:ok ->
Igniter.add_notice(
igniter,
"✅ Copied #{@source_filename} to priv/static/assets/vendor/"
)
{:error, reason} ->
Igniter.add_warning(
igniter,
"⚠️ Failed to copy #{@source_filename}: #{inspect(reason)}. " <>
"Please copy it manually from deps/phoenix_kit/priv/static/assets/#{@source_filename}"
)
end
{:error, :not_found} ->
Igniter.add_warning(
igniter,
"⚠️ Could not find #{@source_filename}. " <>
"Please copy it manually from the phoenix_kit package."
)
end
end
defp add_script_tag_to_layout(igniter) do
layout_paths = [
"lib/#{Mix.Phoenix.otp_app()}_web/components/layouts/root.html.heex",
"lib/#{Mix.Phoenix.otp_app()}_web/templates/layout/root.html.heex"
]
case find_existing_file(layout_paths) do
{:ok, layout_path} ->
content = File.read!(layout_path)
cond do
String.contains?(content, @script_marker) ->
# Already integrated
igniter
String.contains?(content, "phoenix_kit.js") ->
# Already has a script tag (manually added)
igniter
true ->
inject_script_tag(igniter, layout_path, content)
end
{:error, :not_found} ->
Igniter.add_notice(
igniter,
"""
⚠️ Could not find root layout. Please add this before your app.js script tag:
#{@script_marker}
<script src="/assets/vendor/#{@source_filename}"></script>
"""
)
end
end
defp inject_script_tag(igniter, layout_path, content) do
script_tag = """
#{@script_marker}
<script src={~p"/assets/vendor/#{@source_filename}"}></script>\
"""
# Insert before the app.js script tag
updated =
String.replace(
content,
~r{(\s*<script[^>]*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}
<script src={~p"/assets/vendor/#{@source_filename}"}></script>
"""
)
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 <script> tag doesn't 404 before the
# compiler first runs. The compiler overwrites it with real content. A seed
# failure is surfaced as a warning (not swallowed) — the first `mix compile`
# recreates the file, so it's recoverable, but the installer should say so.
defp seed_modules_js_file(igniter) do
dest = Path.join([File.cwd!(), "priv", "static", "assets", "vendor", @modules_filename])
if File.exists?(dest) do
igniter
else
with :ok <- File.mkdir_p(Path.dirname(dest)),
:ok <-
File.write(
dest,
"/* Auto-generated by PhoenixKit — populated on first compile. */\n"
) do
igniter
else
{:error, reason} ->
Igniter.add_warning(
igniter,
"⚠️ Could not seed #{@modules_filename} (#{inspect(reason)}). " <>
"It will be created on the next `mix compile`."
)
end
end
end
# Add the (stable) aggregate-module-hooks <script> tag to the root layout,
# after phoenix_kit.js (so it augments window.PhoenixKitHooks) and before app.js.
defp add_modules_script_tag_to_layout(igniter) do
layout_paths = [
"lib/#{Mix.Phoenix.otp_app()}_web/components/layouts/root.html.heex",
"lib/#{Mix.Phoenix.otp_app()}_web/templates/layout/root.html.heex"
]
case find_existing_file(layout_paths) do
{:ok, layout_path} ->
content = File.read!(layout_path)
if String.contains?(content, @modules_filename) do
igniter
else
inject_modules_script_tag(igniter, layout_path, content)
end
{:error, :not_found} ->
Igniter.add_notice(
igniter,
"""
⚠️ Could not find root layout. Please add this after the phoenix_kit.js
script tag and before your app.js script tag:
#{@modules_script_marker}
<script src={~p"/assets/vendor/#{@modules_filename}"}></script>
"""
)
end
end
defp inject_modules_script_tag(igniter, layout_path, content) do
tag = """
#{@modules_script_marker}
<script src={~p"/assets/vendor/#{@modules_filename}"}></script>\
"""
# 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{<script[^>]*#{pk}[^>]*>\s*</script>}, content) ->
Regex.replace(
~r{(<script[^>]*#{pk}[^>]*>\s*</script>)},
content,
"\\1\n#{tag}",
global: false
)
Regex.match?(~r{(\s*<script[^>]*src=[^>]*app\.js[^>]*>)}, content) ->
Regex.replace(
~r{(\s*<script[^>]*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}
<script src={~p"/assets/vendor/#{@modules_filename}"}></script>
"""
)
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