MishkaCmsCore.Runtime.Resources.Validations.StatefulPlacement (Mishka CMS Core v0.0.1)

Copy Markdown View Source

Refuses markup that places a live component somewhere a live component cannot live.

A stateful component compiles to a Phoenix.LiveComponent, and calling one produces a %Phoenix.LiveView.Component{} — a token that only a LiveView's diff knows how to turn into HTML. Three of the four things this runtime compiles are rendered somewhere no such diff runs, and each failure is worse than an error message:

  • An error page is served by a CONTROLLER. MishkaCmsWeb.ErrorHTML.render/2 calls page_module.render(conn, status) and flattens the result, so the token raises — inside that function's own rescue, which falls back to Phoenix's plain default. The site's custom error page is replaced for every visitor, with a warning in the log and nothing on the screen.
  • A layout wraps error pages as well as pages, so it inherits that.
  • Another component's template is rendered by the builder's canvas through CanvasLive.contain/3, which forces iodata on purpose so a component's raise costs the component and not the author's unsaved session. A live component reached from inside another component's markup builds the token anyway and blows up the PARENT's containment — a red chip on a component the author did not touch.

Where it is enforced

At the action boundary, on the three resources that have somewhere to put markup, so the builder, the Gervaz form, the JSON:API, a seed, a kit installer and a version restore all meet the same rule. A page is the one surface that is a LiveView from end to end, so a page is not checked.

The rule has two ends and both are held. Markup that names a component already live is refused when the markup is written; a component that becomes live while something already names it is refused when IT is written — created live, switched to live, renamed or moved to another site while live, or unarchived while live — with the documents that name it in the message. An archived document is one of those documents: unarchiving it writes none of its markup, so nothing would read that markup then.

Every field that is compiled with the live component/1 in reach

  • a layout — template and helpers[].code
  • an error page — each error_pages[].template, each error_pages[].layout.template, root_layout_template and helpers[].code
  • a component — template, body, helpers[].code, and in extra the prelude and each clauses[].template and clauses[].body

How a component is found in markup

By reading each field in the language it is compiled in, never by a pattern over the source.

HEEx is parsed. MishkaCmsCore.Runtime.PageBuilder.Tree.parse/1 is the same reader the editor uses, so a component_name inside a comment or a code sample is not a placement and a name built at runtime — component_name={@which} — is not a literal this can resolve. The template's own Elixir is read as well, because {component(%{component_name: "x"})} reaches the same function. Markup that will not parse is left alone: Validations.TemplateCompiles is what refuses that, and two messages for one mistake is worse than one.

Elixir — a helper, a body, a prelude — is quoted, and a component call with a literal name is a placement, as is anything its ~H sigils place.

root_layout_template is classic EEx, compiled by Phoenix.HTML.Engine rather than the tag engine, so a <.component> tag in it is TEXT that reaches the visitor as written and calls nothing. What reaches the live function from there is an expression — <%= component(…) %> — and that is what is read.

Only what this write changed

A record whose stored markup predates this validation stays editable. Refusing every other field on it would lock the author out of the one screen the markup gets fixed in.