Compiles a page's code without loading it, so an author is told about a mistake instead of a visitor being shown one.
Why this has to exist
A page's template, events, infos and helpers are all Elixir by the time
MishkaCmsCore.Runtime.Compilers.LiveViewCompiler is done with them, and nothing between the form
and the BEAM reads them first. The failure that follows is quiet in three different ways:
- a page whose
precompileis false — the default — is not compiled when it is saved, so the save reports success andMishkaCmsCore.Runtime.RuntimeStatusreportsReady; - a page whose module IS loaded and then fails to recompile keeps serving the previous version, because the old code is only purged on the success branch;
- the reason is recorded in
MishkaCmsCore.Runtime.States.CompileFailureStateand read by nothing that a human can see.
So the author saves, is told nothing, and the page either does not change or 500s for the first visitor. This module is the missing answer to "did that work?".
Three classes of wrong, and which of them a check can catch
Measured against the real compiler:
{:noreply, socket} parses, compiles, works
{:noreply, socket raises TokenMissingError — caught here, with a line
def def def parses fine, fails when the module is compiled — caught here
"" parses to nothing, compiles to nil, crashes on the first click —
caught here by name, because the message a reader needs is not the
one the runtime would eventually give themThe budget
Compiling a page means expanding use MishkaCmsWeb, :live_view, which is about a second of work,
so the budget is generous: it is here to catch a macro that never returns, not to police a busy
machine. Checks do not wait for each other — each compiles under a name of its own — so the
deadline only ever covers one compile.
Fidelity
The stages are LiveViewCompiler's own — build_assigns/1, build_page_forms/1,
build_event_handlers/1, build_info_handlers/1, build_renders/1, build_helpers/1,
create_module_ast/1 — called directly rather than through build_module/1, which differs only
in firing the runtime:compile:before_page hook and a telemetry span. A check must not fire either, and a check
that reimplemented the stages would eventually pass code the real compile rejects.
Running them one at a time is also what makes the error attributable: each stage owns one group of fragments, so a raise identifies the tab without parsing the message.
Summary
Types
One thing wrong. tab is the group it belongs to, index and name locate it within that group,
and line is a line in the author's own box — each fragment is authored standalone, so there is no
offset to apply.
Functions
Checks one page's code.
One line of prose for a fault, the way it should read above the box it belongs to.
Whether the page's code compiles. For a caller that does not want to render the reasons.
Whether a TEMPLATE is HEEx the compiler will take.
Types
@type fault() :: %{ tab: :template | :events | :infos | :helpers | :page, index: non_neg_integer() | nil, name: String.t() | nil, line: pos_integer() | nil, message: String.t() }
One thing wrong. tab is the group it belongs to, index and name locate it within that group,
and line is a line in the author's own box — each fragment is authored standalone, so there is no
offset to apply.
Functions
Checks one page's code.
Takes the same shape LiveViewCompiler takes — :template, :events, :infos, :state,
:helpers — and fills in the rest, because none of it affects whether the code compiles.
Returns :ok, or every fault it can attribute. Fragment faults are reported together so an author
fixing three typos is told about three typos; a module-level fault is reported alone, because
everything after the first one is noise.
One line of prose for a fault, the way it should read above the box it belongs to.
Whether the page's code compiles. For a caller that does not want to render the reasons.
Whether a TEMPLATE is HEEx the compiler will take.
THE TOKENIZER IS NOT THE COMPILER, and until this existed the tokenizer was the only thing the
editor asked. MishkaCmsCore.Runtime.PageBuilder.Tree.parse/1 accepts {@a + } — the braces
delimit an expression and the expression is a token to it — and HEEx refuses it. So hand-written
markup went into the document, into a draft and into the live record, and the failure surfaced
later as a site that would not compile, in a log nobody was looking at.
This is check/1's syntax half on its own: Phoenix.LiveView.TagEngine.compile/2, which is the
call every one of this runtime's compilers makes, with no module built around it. It costs
milliseconds rather than the second a full page compile does, which is what lets it sit in front of
every Apply in the code drawer rather than only in front of a publish.
What it CANNOT catch is anything semantic — an undefined function, a component called with the
wrong attrs — because none of that is decided until the module is compiled. check/1 is where
those are caught, for a page; the class this catches is the class the tokenizer lets through.