Uh oh!
There was an error while loading. Please reload this page.
Warn when a module's JS hooks will never be loaded - #703
Merged
Conversation
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 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 thatconsumes that declaration is the
:phoenix_kit_js_sourcescompiler. A hostwithout it in
:compilersgets nothing — no vendored bundle, no error, nowarning.
window.PhoenixKitHooksnever gains those hooks, while the module'stemplates go on rendering
phx-hook="..."names the LiveSocket has never heardof.
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_boardsshipped 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 oneplace 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:
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:
Not true since LiveView 1.1.
getHookDefinition/1resolves a hook name when itselement mounts and falls back to a
script[data-phx-runtime-hook="Name"]in thedocument; 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_boardsdoes now. Leaving the old reasoning in place tells thenext 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 --strictclean, compiles with--warnings-as-errors.Scope
Only #7 (plus its moduledoc). On the rest of the report, three corrections worth
recording:
phoenix_kit_comments(0.2.13). The resource model and the zero-width-spaceGIF placeholder both live there.
Auth.merge_user_custom_fields/3does the atomicCOALESCE(?, '{}'::jsonb) || ?merge the report asks for, and its docstringsays to reach for it whenever the intent is "add/update these keys, leave
everything else as any concurrent writer left it". There is a
delete_user_custom_field/3too. The host's 13 call sites are calling thewrong function — a real race in that app, a discoverability problem here. A
one-key
put_user_custom_field/3convenience would be a fair addition.🤖 Generated with Claude Code
https://claude.ai/code/session_01NTj7hm3fpCTcFvKLRtgppW