Packages
phoenix_kit
1.7.192
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/phoenix_kit/module_discovery.ex
defmodule PhoenixKit.ModuleDiscovery do
@moduledoc """
Zero-config auto-discovery of external PhoenixKit modules.
Uses the same pattern as Elixir's protocol consolidation: scans `.beam` files
for persisted `@phoenix_kit_module` attributes via `:beam_lib.chunks/2`.
No module loading required — pure file I/O.
## How It Works
1. `use PhoenixKit.Module` persists `@phoenix_kit_module true` in the `.beam` file
2. This module scans only deps that depend on `:phoenix_kit` (fast, targeted)
3. Reads the persisted attribute from each beam file without loading the module
4. Works at both compile time (route generation) and runtime (ModuleRegistry)
## Fallback
Also reads `Application.get_env(:phoenix_kit, :modules, [])` for backwards
compatibility. Both sources are merged and deduplicated.
"""
require Logger
@doc """
Discovers external PhoenixKit modules from beam files + config fallback.
Returns a deduplicated list of module atoms that implement `PhoenixKit.Module`.
Excludes internal modules (those in the `PhoenixKit.Modules` namespace that are
bundled with PhoenixKit itself).
"""
@spec discover_external_modules() :: [module()]
def discover_external_modules do
scanned = scan_beam_files()
configured = Application.get_env(:phoenix_kit, :modules, [])
Enum.uniq(scanned ++ configured)
end
@doc """
Returns a deterministic hash of the current set of discovered external modules.
Used by `__mix_recompile__?/0` (injected into the host router) to detect when
modules are added or removed, triggering router recompilation.
"""
@spec module_hash() :: binary()
def module_hash do
discover_external_modules()
|> Enum.sort()
|> :erlang.term_to_binary()
|> then(&:erlang.md5/1)
end
@doc """
Scans beam files of phoenix_kit-dependent apps for `@phoenix_kit_module` attribute.
Walks dependency `ebin` directories on disk (pure file I/O) rather than relying
on `:application.loaded_applications/0`, so it is deterministic at compile time —
it returns the same set whether or not the apps happen to be loaded yet. An app
qualifies when its `<app>.app` lists `:phoenix_kit` in `applications`; its beams
are then read with `:beam_lib.chunks/2` to keep the ones carrying
`@phoenix_kit_module true`. No module loading required.
"""
@spec scan_beam_files() :: [module()]
def scan_beam_files do
phoenix_kit_dependent_ebin_dirs()
|> Enum.flat_map(&beam_modules_in_dir/1)
|> Enum.uniq()
rescue
error ->
Logger.warning("[ModuleDiscovery] Beam scanning failed: #{Exception.message(error)}")
[]
end
@doc """
Returns the names of dependency apps on disk that declare `:phoenix_kit` in their
`applications` (i.e. via `extra_applications`).
Filesystem-based, independent of load state. Used by the CSS-sources compiler to
warn when discovery yields zero sources even though phoenix_kit-dependent deps
are present.
"""
@spec phoenix_kit_dependent_apps() :: [atom()]
def phoenix_kit_dependent_apps do
phoenix_kit_dependent_ebin_dirs()
|> Enum.map(&app_name_for_ebin/1)
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
rescue
_ -> []
end
# ebin directories whose `<app>.app` depends on :phoenix_kit (excludes phoenix_kit itself).
defp phoenix_kit_dependent_ebin_dirs do
candidate_ebin_dirs()
|> Enum.filter(&ebin_depends_on_phoenix_kit?/1)
end
# All ebin directories that might hold compiled deps. The code path covers both
# compile time and runtime: during `mix compile` the `deps.loadpaths` task prepends
# every dep's ebin to the code path *before* compilers run, so freshly compiled deps
# are present even on a cold build (`rm -rf _build`); at runtime it holds the loaded
# apps' ebins. Crucially this is independent of `:application.loaded_applications/0`,
# which is what made discovery nondeterministic at compile time.
defp candidate_ebin_dirs do
:code.get_path()
|> Enum.map(&List.to_string/1)
|> Enum.uniq()
rescue
_ -> []
end
defp ebin_depends_on_phoenix_kit?(dir) do
case read_app_spec(dir) do
{app, keys} ->
app != :phoenix_kit and :phoenix_kit in Keyword.get(keys, :applications, [])
nil ->
false
end
end
defp app_name_for_ebin(dir) do
case read_app_spec(dir) do
{app, _keys} -> app
nil -> nil
end
end
# Reads the `<app>.app` resource file from an ebin dir as `{app_name, keys}`.
# Pure file read — does not load the application.
defp read_app_spec(dir) do
with [app_file | _] <- Path.wildcard(Path.join(dir, "*.app")),
{:ok, [{:application, app, keys}]} <- :file.consult(String.to_charlist(app_file)) do
{app, keys}
else
_ -> nil
end
rescue
_ -> nil
end
defp beam_modules_in_dir(dir) do
dir
|> Path.join("*.beam")
|> Path.wildcard()
|> Enum.map(&beam_phoenix_kit_module/1)
|> Enum.reject(&is_nil/1)
end
# Reads the persisted `@phoenix_kit_module` attribute via :beam_lib.chunks/2
# without loading the module. Returns the module atom (which :beam_lib resolves
# from the beam itself, so no String.to_existing_atom fragility) or nil.
defp beam_phoenix_kit_module(path) do
case :beam_lib.chunks(String.to_charlist(path), [:attributes]) do
{:ok, {module, [{:attributes, attrs}]}} ->
if attrs[:phoenix_kit_module] == [true], do: module
_ ->
nil
end
end
end