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
use PhoenixKit.Modulepersists@phoenix_kit_module truein the.beamfile- This module scans only deps that depend on
:phoenix_kit(fast, targeted) - Reads the persisted attribute from each beam file without loading the module
- 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.
Summary
Functions
Like discover_external_modules/0, but scans the disk once per VM and then
answers from :persistent_term.
Drops the cached scan; the next cached_external_modules/0 call scans again.
Discovers external PhoenixKit modules from beam files + config fallback.
Returns a deterministic hash of the current set of discovered external modules.
Same answer as module_hash/0, without re-reading the beams when nothing on
disk changed since the last call.
Returns the names of dependency apps on disk that declare :phoenix_kit in their
applications (i.e. via extra_applications).
Scans the disk now (discover_external_modules/0), stores the result for
cached_external_modules/0 and returns it.
Scans beam files of phoenix_kit-dependent apps for @phoenix_kit_module attribute.
Functions
@spec cached_external_modules() :: [module()]
Like discover_external_modules/0, but scans the disk once per VM and then
answers from :persistent_term.
Runtime callers only (the admin Modules page, PhoenixKit.ModuleRegistry).
The scan reads every beam file of every phoenix_kit-dependent dep, which takes
seconds on a cold or busy disk, while the set it finds only changes when the
release is rebuilt. Compile-time callers (router macros, module_hash/0, the
Mix compilers) must keep using discover_external_modules/0: a long-lived
iex -S mix / code-reloader VM recompiles against a changing disk and has to
see it.
The cache is dropped by clear_cache/0 and rebuilt by refresh_cache/0.
PhoenixKit.ModuleRegistry refreshes it at boot and on rescan/0 and drops it
on register/1 / unregister/1. Concurrent cold readers share one scan. The
cache also reflects config :phoenix_kit, :modules as of the scan.
@spec clear_cache() :: :ok
Drops the cached scan; the next cached_external_modules/0 call scans again.
@spec discover_external_modules() :: [module()]
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 module_hash() :: binary()
Returns a deterministic hash of the current set of discovered external modules.
The exact reference for module_hash_fast/0, which is what __mix_recompile__?/0
(injected into the host router) calls to detect when modules are added or
removed, triggering router recompilation. Always scans the disk — it must
never be answered from the runtime cache.
@spec module_hash_fast() :: binary()
Same answer as module_hash/0, without re-reading the beams when nothing on
disk changed since the last call.
Meant for __mix_recompile__?/0, which Mix evaluates on every compile — and
Phoenix.CodeReloader compiles on every dev request, so module_hash/0 there
re-ran the whole scan per HTTP request.
It fingerprints what the scan reads, with stat only — no beam is opened and no
directory is listed: the mtime of every code-path directory (a file added to or
removed from one changes it), plus mtime and size of the .app files and of the
beams of the dependent apps found by the last full scan (a dep that starts or
stops depending on :phoenix_kit, a module gaining or losing
@phoenix_kit_module), and config :phoenix_kit, :modules. The full scan runs
when the fingerprint differs from the remembered one, and nothing is remembered
while any of it is newer than 2 seconds, so a change
within the filesystem's timestamp resolution is never missed.
The assumption is that a rebuild leaves a new mtime or size on the files it
writes, as Mix and erlc do. A file replaced by one of the same size and the
same mtime (cp -p, rsync -t from an older build) is not seen — use
module_hash/0 where that matters.
A miss costs one scan plus the stats of the fingerprint (it builds the hash
from the same directory listings the fingerprint needs, so nothing is listed or
read twice); the first call of a VM is always a miss. Any error falls back to
module_hash/0.
@spec phoenix_kit_dependent_apps() :: [atom()]
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 refresh_cache() :: [module()]
Scans the disk now (discover_external_modules/0), stores the result for
cached_external_modules/0 and returns it.
@spec scan_beam_files() :: [module()]
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.