Packages
phoenix_kit
1.7.162
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 bundles external-module JavaScript hooks for 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. Modules
declare their prebuilt hook bundles via `js_sources/0` (see `PhoenixKit.Module`);
this compiler discovers them and writes one aggregate file the host loads with a
single `<script>` tag (added once by `mix phoenix_kit.install`).
## What it does (every compile)
1. Discovers external modules via `PhoenixKit.ModuleDiscovery`, collects their
`js_sources/0` entries.
2. Resolves each bundle's absolute path via `:code.priv_dir/1` — so it works for
Hex installs and path deps alike, with no `deps/<app>` path arithmetic.
3. Concatenates the bundles (each wrapped in an IIFE so their top-level scopes
can't collide) into `priv/static/assets/vendor/phoenix_kit_modules.js`,
followed by a merge that folds each bundle's `window.<Global>` into
`window.PhoenixKitHooks` (which the host already spreads into `LiveSocket`).
4. Writes only when the content changed (diff-before-write) to avoid live-reload
thrash.
The output tag is **stable**: adding or removing a JS-bearing module only changes
this file'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_modules.js"}></script>
Failures are loud: a declared bundle that can't be resolved raises a compile
error rather than silently shipping a chart with "unknown hook" console errors.
"""
use Mix.Task.Compiler
require Logger
@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
specs = collect_specs()
content = build_content(specs)
path = Path.join(File.cwd!(), @output)
write_if_changed(path, content, length(specs))
{:ok, []}
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