How widgets run
How a widget tag in Markdown becomes a mounted React component: the
placeholder the server writes, how widgets are compiled and served, and how the
page script mounts them. It is a mechanism page under Widgets,
for someone about to change src/widgets.rs or the widget code in
assets/devpage.js.
flowchart LR
tag["Tag in Markdown"] --> placeholder["Placeholder<br/>on the server"]
placeholder --> fetch["Module fetched<br/>compiled by oxc"]
fetch --> mount["Mounted<br/>with React"]
click placeholder href "#on-the-server" "On the server"
click fetch href "#compiling" "Compiling"
click mount href "#in-the-browser" "In the browser"
On the server
In src/widgets.rs, rendering a page finds the tags of known
widgets in its Markdown — skipping fenced, indented and inline code and HTML
comments — and turns each into
a placeholder: a div for a block widget, a span for an inline one, so an
inline widget never breaks its paragraph. The placeholder carries the widget's
name, its props as a JavaScript object literal (quoted values as strings, {…}
as written, bare attributes as true) and its Markdown source — and, for
Example, that source highlighted as Markdown by the same highlighter fenced
code uses, which reaches it as the sourceHtml prop. Its content stays between
the tags for comrak to render as Markdown. The page's <head> gets an import map
naming React, and the list of the document's widgets with the address of each.
Compiling
A widget or helper is compiled from TSX by oxc when the
browser asks for it, with React's automatic JSX runtime. Capitalized names a
widget uses without declaring them are other widgets: the compiled module looks
each up by name when it renders, and lists them in __devpageUses. A system
widget composes only system widgets; a project widget composes the system
widgets and those of the document whose widgets folder holds it. Every project
widget's address carries its folder's version — the latest change time of
the folder and its .ts and .tsx files — and so does every relative import
inside it, so an edited widget or helper is fetched afresh rather than taken
from the browser's module cache. Compiled modules are remembered, keyed by the
file, its source, the names it can use and the version, so a widget compiles
once per change, not once per request. A file that doesn't compile is served as
a module that throws the compiler's message, and so is a widget whose relative
imports reach a helper that doesn't compile — with the helper's message — so
the page shows why in the widget's place rather than only that its import
failed.
| Address | What it serves |
|---|---|
/__devpage/react.js |
React and react-dom, vendored, for the import map's react, react/jsx-runtime and react-dom/client |
/__devpage/system-widgets/<Name>.tsx |
A system widget, compiled from assets/widgets/, built into the binary |
/__devpage/widgets/<path>?v=<version> |
A project widget or helper at that path in the served folder, compiled; an import without an extension finds the .tsx or .ts |
In the browser
In assets/devpage.js, each pre[lang="mermaid"] first
becomes a Diagram placeholder. Then the page script loads only the widgets the
content names, nested ones included, and those each lists in __devpageUses;
adds a widget's exported css to the page once; and mounts each outermost
placeholder with React. Nested widgets become React elements their parent can
read — a Tabs reads its Tabs' titles — and the outermost widget is wrapped
in an error boundary that shows a failure in its place; a widget whose module
didn't load shows the reason it didn't. Each widget gets its
props, children, source and html. Mounting happens on page load and again
after every live update, which takes the fresh page's widget list, so new
versions are fetched. The page marks itself ready only once every widget has
mounted, which is what devpage pdf waits for.
Widget and helper files refresh open pages whatever a site's watch list says
(Live updates).