Packages
phoenix_kit
1.7.185
1.7.210
1.7.209
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
Current section
Files
lib/mix/tasks/compile.phoenix_kit_js_sources.ex
defmodule Mix.Tasks.Compile.PhoenixKitJsSources do
@moduledoc """
Mix compiler that vendors PhoenixKit's JS hooks into the host app.
A LiveView JS hook must be present in the host's single `LiveSocket` at
construction time — a nested LiveView cannot register one at runtime. This
compiler keeps two vendored files current on **every** `mix compile`:
1. `priv/static/assets/vendor/phoenix_kit.js` — a straight copy of
PhoenixKit's own core hooks file. Previously this was only copied once,
by `mix phoenix_kit.install`/`mix phoenix_kit.update` (`File.cp/2` in
`PhoenixKit.Install.JsIntegration`) — a deploy that does `rm -rf
priv/static` + rebuild without re-running `phoenix_kit.update` (i.e. most
CI/CD pipelines) shipped a 404 on this file, silently breaking every
PhoenixKit JS hook. Vendoring it from a compiler makes it self-healing:
it comes back on the next `mix compile`, with zero dependency on
`phoenix_kit.update` ever having run again after install.
2. `priv/static/assets/vendor/phoenix_kit_modules.js` — external modules
declare prebuilt hook bundles via `js_sources/0` (see `PhoenixKit.Module`);
this compiler discovers them via `PhoenixKit.ModuleDiscovery`, resolves
each bundle's absolute path via `:code.priv_dir/1` (works for Hex installs
and path deps alike), and concatenates them (each wrapped in an IIFE so
their top-level scopes can't collide) into this file, followed by a merge
that folds each bundle's `window.<Global>` into `window.PhoenixKitHooks`
(which the host spreads into `LiveSocket`).
Both writes are diffed against the existing file first, so a `mix compile`
that changes nothing doesn't touch either file (avoids live-reload thrash).
The two `<script>` tags are **stable**: adding or removing a JS-bearing
module only changes `phoenix_kit_modules.js`'s content on the next compile —
never the host's layout — so it stays zero-config like `css_sources/0`.
## Setup (one-time, by `mix phoenix_kit.install`)
# mix.exs
compilers: [:phoenix_kit_js_sources, :phoenix_live_view] ++ Mix.compilers()
# root.html.heex, before app.js
<script src={~p"/assets/vendor/phoenix_kit.js"}></script>
<script src={~p"/assets/vendor/phoenix_kit_modules.js"}></script>
Failures are loud: a missing core JS file (corrupted/incomplete dependency
fetch) or a declared module bundle that can't be resolved raises a compile
error rather than silently shipping "unknown hook" console errors.
"""
use Mix.Task.Compiler
require Logger
@core_js_filename "phoenix_kit.js"
@core_js_output "priv/static/assets/vendor/phoenix_kit.js"
@output "priv/static/assets/vendor/phoenix_kit_modules.js"
@impl true
def run(_args) do
# Ensure all dep applications are loaded so module discovery + priv_dir work.
for dep <- Mix.Dep.cached() do
Application.ensure_loaded(dep.app)
end
vendor_core_js_file()
specs = collect_specs()
content = build_content(specs)
path = Path.join(File.cwd!(), @output)
write_if_changed(path, content, length(specs))
{:ok, []}
end
# ── Core hooks file vendoring ───────────────────────────────────────
# Copies PhoenixKit's own priv/static/assets/phoenix_kit.js into the host's
# vendor/ directory. Runs on every compile — unlike the old one-shot
# `File.cp/2` in `phoenix_kit.install`/`phoenix_kit.update`, this survives a
# `rm -rf priv/static` in a deploy pipeline that only runs `mix compile`.
defp vendor_core_js_file do
priv =
case :code.priv_dir(:phoenix_kit) do
{:error, :bad_name} ->
Mix.raise("""
Could not resolve the :phoenix_kit application's priv/ directory
while vendoring #{@core_js_filename}. Is :phoenix_kit listed as a
dependency of this app?
""")
dir ->
to_string(dir)
end
source = Path.join([priv, "static", "assets", @core_js_filename])
unless File.exists?(source) do
Mix.raise("""
Could not find #{@core_js_filename} at #{source} — the :phoenix_kit
dependency looks corrupted or incompletely fetched. Try `mix deps.get`.
""")
end
content = File.read!(source)
dest = Path.join(File.cwd!(), @core_js_output)
existing =
case File.read(dest) do
{:ok, data} -> data
_ -> nil
end
if existing != content do
File.mkdir_p!(Path.dirname(dest))
File.write!(dest, content)
Mix.shell().info("[PhoenixKit] Vendored #{@core_js_output}")
end
end
# ── Discovery ────────────────────────────────────────────────────
# Returns a deterministic list of %{app, file, global, source} maps.
defp collect_specs do
PhoenixKit.ModuleDiscovery.discover_external_modules()
|> Enum.flat_map(fn mod ->
if Code.ensure_loaded?(mod) and function_exported?(mod, :js_sources, 0) do
mod.js_sources()
else
[]
end
end)
|> Enum.map(&normalize_entry/1)
# Deterministic order; dedupe identical (app, file) declarations.
|> Enum.uniq_by(fn s -> {s.app, s.file} end)
|> Enum.sort_by(fn s -> {to_string(s.app), s.file} end)
|> tap(&check_unique_globals/1)
|> Enum.map(&resolve_source/1)
end
# Two distinct bundles assigning the same window.<Global> would clobber each
# other (last bundle wins; the merge dedupes the global), silently dropping the
# earlier module's hooks. Fail loud instead.
@doc false
def check_unique_globals(specs) do
dupes =
specs
|> Enum.group_by(& &1.global)
|> Enum.filter(fn {_global, entries} -> length(entries) > 1 end)
unless dupes == [] do
detail =
Enum.map_join(dupes, "\n", fn {global, entries} ->
apps = Enum.map_join(entries, ", ", fn e -> "#{e.app}:#{e.file}" end)
" window.#{global} <- #{apps}"
end)
Mix.raise("""
Multiple js_sources/0 bundles declare the same window global, so the later
bundle would clobber the earlier one's hooks:
#{detail}
Each module's bundle must assign a unique window.<Global>. Rename one.
""")
end
:ok
end
@doc false
def normalize_entry(%{app: app, file: file, global: global})
when is_atom(app) and is_binary(file) and is_binary(global) do
# `global` is emitted as `window.<global>` in the generated JS, so an
# invalid identifier (e.g. "foo-bar") would produce broken JS — fail loud.
unless Regex.match?(~r/^[A-Za-z_$][A-Za-z0-9_$]*$/, global) do
Mix.raise("""
Invalid js_sources/0 :global #{inspect(global)} — must be a valid JavaScript
identifier (it is emitted as window.<global> and folded into PhoenixKitHooks).
""")
end
%{app: app, file: file, global: global}
end
def normalize_entry(other) do
Mix.raise("""
Invalid js_sources/0 entry: #{inspect(other)}
Each entry must be a map with :app (atom), :file (string, relative to the
app's priv/), and :global (string, the window.<Name> the bundle assigns).
""")
end
# Resolve the bundle's absolute path via the app's priv dir. Fail loud.
defp resolve_source(%{app: app, file: file} = spec) do
priv =
case :code.priv_dir(app) do
{:error, :bad_name} ->
Mix.raise("""
js_sources/0 references app #{inspect(app)}, but it isn't an available
application. Add it as a dependency of the module declaring it.
""")
dir ->
to_string(dir)
end
source = Path.join(priv, file)
unless File.exists?(source) do
Mix.raise("""
js_sources/0 bundle not found: #{source}
App #{inspect(app)} declared js_sources file #{inspect(file)} (resolved under
its priv/), but no such file exists. The bundle must ship in the app's priv/.
""")
end
Map.put(spec, :source, source)
end
# ── Content ──────────────────────────────────────────────────────
@doc false
def build_content([]) do
"""
/* Auto-generated by PhoenixKit — do not edit manually.
No external module declares js_sources/0; this file is intentionally empty. */
"""
end
def build_content(specs) do
bundles =
Enum.map_join(specs, "\n", fn %{app: app, source: source} ->
body = File.read!(source)
# Wrap each prebuilt bundle in an IIFE so its top-level declarations
# can't collide with another bundle's. Bundles export via window.<Global>,
# which survives the wrapper. Leading `;` guards against ASI hazards.
"""
/* #{app} */
;(function(){
#{body}
})();
"""
end)
globals =
specs
|> Enum.map(& &1.global)
|> Enum.uniq()
|> Enum.map_join(",", fn g -> "window.#{g}||{}" end)
# NOTE: `check_unique_globals/1` guarantees distinct window.<Global> names,
# but this Object.assign is still last-write-wins on the HOOK NAMES inside
# each bundle — and it folds over the core window.PhoenixKitHooks too. Two
# modules exporting a same-named hook (or one shadowing a core hook) clobber
# silently; we can't detect that without parsing the bundles. js_sources/0's
# docs tell modules to namespace their hook names accordingly.
"""
/* Auto-generated by PhoenixKit — do not edit manually.
Concatenated module hook bundles (js_sources/0), loaded before app.js. */
#{bundles}
/* Fold each module's hooks into window.PhoenixKitHooks (spread into LiveSocket by app.js). */
window.PhoenixKitHooks=Object.assign(window.PhoenixKitHooks||{},#{globals});
"""
end
defp write_if_changed(path, content, spec_count) do
existing =
case File.read(path) do
{:ok, data} -> data
_ -> nil
end
if existing != content do
File.mkdir_p!(Path.dirname(path))
File.write!(path, content)
Mix.shell().info("[PhoenixKit] Updated #{@output} with #{spec_count} module hook bundle(s)")
end
end
end