Skip to content

Warn when a module's JS hooks will never be loaded - #703

Merged
ddon merged 1 commit into
BeamLabEU:mainfrom
alexdont:js-sources-compiler-warning
Aug 11, 2026
Merged

Warn when a module's JS hooks will never be loaded#703
ddon merged 1 commit into
BeamLabEU:mainfrom
alexdont:js-sources-compiler-warning

Conversation

@alexdont

Copy link
Copy Markdown
Contributor

From the integration report, item #7 — the one the boards 0.3.0 and 0.4.0
incidents were both symptoms of.

The trap

A module declares its hook bundle with js_sources/0. The only thing that
consumes that declaration is the :phoenix_kit_js_sources compiler. A host
without it in :compilers gets nothing — no vendored bundle, no error, no
warning. window.PhoenixKitHooks never gains those hooks, while the module's
templates go on rendering phx-hook="..." names the LiveSocket has never heard
of.

That is the worst shape a failure can take. The page renders, the module looks
installed, and only the half that needed JavaScript is missing — so it reads as
"this module is broken" rather than "its JS never loaded". phoenix_kit_boards
shipped exactly that state twice before anyone traced it, and a host running the
CSS compiler but not the JS one (the reporter's app, and an easy asymmetry to
end up with) lands there without ever having made a decision.

The warning

phoenix_kit_routes() runs while the host's router compiles, which is the one
place that can see both facts: what is installed, and what is in :compilers.
When any installed module declares a bundle and the compiler is absent, it names
the modules and the line that fixes it.

Silent when the compiler is present, and silent when nothing declares a bundle.
That restraint is the point — a warning that fires on correctly-configured hosts
is one people learn to scroll past, which is how we got here.

Verified against a real host both ways:

=== compiler PRESENT (expect silence) ===
0
=== compiler REMOVED (expect the warning) ===
warning: PhoenixKit: these modules ship JavaScript hooks that will not be loaded:
PhoenixKitBoards
PhoenixKitReferrals

It can never fail a host's compile: discovery is rescued, and the Mix lookup is
guarded for contexts where Mix isn't loaded.

The stale rationale

The compiler's moduledoc opened with:

A LiveView JS hook must be present in the host's single LiveSocket at
construction time — a nested LiveView cannot register one at runtime.

Not true since LiveView 1.1. getHookDefinition/1 resolves a hook name when its
element mounts and falls back to a script[data-phx-runtime-hook="Name"] in the
document; LiveView re-creates such a script when a patch adds it, so it works for
a page delivered over the socket too. (Verified against the 1.2.x source while
building the boards fix, not taken on trust.)

So a module can deliver its own hooks and ask nothing of the host — which is
what phoenix_kit_boards does now. Leaving the old reasoning in place tells the
next module author that host wiring is unavoidable when it isn't. Rewritten to
say what the compiler actually is: the cheaper path where it's set up, not the
only one.

Testing

Six tests on the decision itself. The first version reached it through real
module discovery, which meant the interesting branch only ran on a host that
happened to be misconfigured — i.e. never, in this library's own suite. Split
into a two-argument form so both branches are exercised directly.

Both directions mutation-tested: silencing the warning fails three tests,
removing the "configured" guard fails one. (My first attempt at the former was
invalid Elixir and only produced a compile error, so it proved nothing — redone.)

705 web tests pass, credo --strict clean, compiles with --warnings-as-errors.

Scope

Only #7 (plus its moduledoc). On the rest of the report, three corrections worth
recording:

🤖 Generated with Claude Code

https://claude.ai/code/session_01NTj7hm3fpCTcFvKLRtgppW

A module declares its hook bundle with `js_sources/0`, and the only thing
that consumes that declaration is the `:phoenix_kit_js_sources` compiler. A
host without it in `:compilers` gets nothing — no vendored bundle, no error,
no warning. `window.PhoenixKitHooks` simply never gains those hooks while
the module's templates go on rendering `phx-hook="..."` names the LiveSocket
has never heard of.
That is the worst available shape for a failure. The page renders, the
module looks installed, and only the half that needed JavaScript is missing,
so it reads as "this module is broken" rather than "its JS never loaded".
phoenix_kit_boards shipped that state twice before anyone traced it, and a
host that runs the CSS compiler but not the JS one — an easy asymmetry to
end up with — lands in it without ever having made a decision.
Cheaply detectable, so it is now detected: the routes macro runs while the
host's router compiles, which is the one place that can see both facts. If
any installed module declares a bundle and the compiler is absent, it names
the modules and the line that fixes it. Silent, as before, when the compiler
is there or when nothing declares a bundle — a warning that fires on
correctly-configured hosts is one people learn to scroll past.
Verified against a real host both ways: silent as configured, and naming
PhoenixKitBoards and PhoenixKitReferrals with the compiler removed.
Also corrects the compiler's own rationale, which had been overtaken by
LiveView 1.1. "A hook must be present in the host's LiveSocket at
construction time — a nested LiveView cannot register one at runtime" is no
longer true: `getHookDefinition/1` resolves a hook when its element mounts
and falls back to a runtime-hook script in the document. A module can
therefore deliver its own hooks and ask nothing of the host, which is what
phoenix_kit_boards now does. Leaving the old reasoning in place tells the
next module author that host wiring is unavoidable when it isn't.
@ddon
ddon merged commit e71bc5b into BeamLabEU:mainAug 11, 2026
ddon pushed a commit that referenced this pull request Aug 11, 2026
Review docs for all three PRs in dev_docs/pull_requests/2026/.
#704 — two bugs on the read-then-write race in `upsert_inapp/3`:
* The `update_all` filtered on `uuid` alone, so the unseen/undismissed rule
held only in the read. A row dismissed or read in between was refreshed
anyway — the update reported success and the event ended up recorded only
on a row that will never be shown again. The guard now appears in the
write too, which also makes the fallback reachable for the reason it was
written.
* The fallback posted the replacement row without its dedupe key, so it
could never be found again: every later event for that key opened a new
row. One lost race turned collapsing off permanently.
#705 — the level-follows-consequence rule was applied to one branch of three.
Strict mode refuses every mismatch (`PhoenixKitWeb.Users.Auth` answers
`{:warning, _}` exactly as it answers `{:error, _}`), so a strict-mode host
logging someone out over a browser update recorded it at `:info` — below the
default threshold, i.e. not at all. Extracted `log_mismatch/2`; non-strict
behaviour is unchanged in all three branches, which the untouched existing
tests demonstrate.
#703 — the warning was emitted with `IO.warn/1`, which registers a compiler
diagnostic and so failed the build on any host using `--warnings-as-errors`,
on upgrade, over a mix.exs condition that is not a regression in their code.
That contradicted the PR's own guarantee and the rescue guards written to
back it up. Now written straight to stderr; pinned by a test asserting no
diagnostic is registered.
Also updates deps (phoenix 1.8.10, hackney 4.7.3) and corrects the
`version/0` doc example, which still claimed "1.3.3".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@alexdont@ddon