PhoenixKit.ModuleDiscovery (phoenix_kit v2.55.1)

Copy Markdown View Source

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.

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

cached_external_modules()

@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.

clear_cache()

@spec clear_cache() :: :ok

Drops the cached scan; the next cached_external_modules/0 call scans again.

discover_external_modules()

@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).

module_hash()

@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.

module_hash_fast()

@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.

phoenix_kit_dependent_apps()

@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.

refresh_cache()

@spec refresh_cache() :: [module()]

Scans the disk now (discover_external_modules/0), stores the result for cached_external_modules/0 and returns it.

scan_beam_files()

@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.