Hono host
A Hono JSX component with MX in place of JSX. The file below is an ordinary hono/jsx function component: it returns Hono JSX, takes props, and is rendered by Hono’s own runtime in your routes. MX replaces only the JSX. Because the template is the whole file, there is no function wrapper to write: the compiler emits the default-exported function component and your export interface Input becomes its props type. Hono renders on the server, so the example has no event handler.
export interface Input {
label: string;
names: { id: number; text: string }[];
}
export default function Greeter({ label, names }: Input) {
return (
<section>
<h1>{label}</h1>
{names.length > 2 && <p>That is plenty.</p>}
<ul>
{names.map((name) => (
<li key={name.id}>{name.text}</li>
))}
</ul>
</section>
);
}
export interface Input {
label: string;
names: { id: number; text: string }[];
}
<section>
<h1>${input.label}</h1>
<if=(input.names.length > 2)><p>That is plenty.</p></if>
<ul>
<for|name| of=input.names by="id"><li>${name.text}</li></for>
</ul>
</section>
@mxlang/hono compiles a .mx or .marko template to a Hono JSX component
module. Structural MX becomes ordinary hono/jsx TSX: <if> becomes a
ternary, <for> becomes .map() with a key, component children are JSX
children, and attribute tags become props.
Selecting the host
mx.host: "hono" selects hono-jsx; mx.target: "hono-jsx" alone selects
Hono behaviour too. If both keys are given, the target must belong to Hono;
disagreement is a positioned target-host-mismatch error (decisions 129/132;
spec §13.5).
Setup
{
"dependencies": {
"@mxlang/hono": "*",
"hono": "4.6.20"
},
"mx": { "host": "hono" }
}
For a plain Bun server with no bundler, preload the Bun loader:
# bunfig.toml
preload = ["@mxlang/hono/bun"]
import { Hono } from "hono";
import App from "./App.mx";
const app = new Hono();
app.get("/", async (c) => c.html(await App({}).toString()));
The same host policy drives Vite, editor diagnostics, and mx-tsc. See
examples/hono-app for the complete app.
Hono-specific output
The host emits /** @jsxImportSource hono/jsx */ and keeps class/for
native — Hono, like Preact, accepts them directly with no renaming. It uses
dangerouslySetInnerHTML={{ __html: value }} for a sole raw placeholder.
Structured class objects and arrays go through mxClass so they remain Marko
class semantics instead of rendering as [object Object].
Hono shares the same structural emitter as Preact and React. @mxlang/hono
depends on @mxlang/preact and supplies a Hono dialect object containing only
the names that differ. Unlike Preact and React, it ships no hand-rolled
error boundary or suspense component — Hono’s hono/jsx provides
ErrorBoundary and Suspense natively, so this host’s runtime supplies only
mxClass.
Attribute-tag contracts import AttrTag from @mxlang/hono. A renderable is
a Hono Child; data is { ...attrs, ...nestedTags, content?: Child }. Params
turn either renderable position into a function returning Child, and repeats
are real arrays. See AttrTag for the shared contract and
the decision-108 fallback used by untyped library components.
Events
An element’s on<Name>=fn (onClick, onDblClick) or on-<exact>=fn
(on-my-event) is an event handler. MX derives the DOM event name —
everything after on lowercased, or the exact text after on- — and this
host emits a JSX prop recomposed from it: on plus the capitalized DOM
name. onClick=f → onClick={f}; onDblClick=f and on-dblclick=f both
→ onDblclick={f} (hono lowercases the prop name at bind time, so it binds dblclick).
- No aliases.
onDoubleClicklowercases todoubleclick, which is not a DOM event: the compiler warns at the attribute and emitsonDoubleclick={f}exactly as written. - Custom DOM events (
on-my-event=f) are a compile error naming the portable route: arefcallback callingaddEventListener("my-event", fn). The error is uniform across Preact, React and hono, so the same MX source never binds on one and dies on another. onChangebinds theinputevent, for React compatibility — on every keystroke, not on commit the waychangefires elsewhere. If you need one behaviour, say so:onInputfor per-keystroke, or handlechange’s timing in the handler. This is hono’s runtime semantics, documented not shimmed; see the gotcha in Attributes.- Static strings (
onClick="alert(1)") are an ordinary attribute and pass through verbatim; MX does not invent a policy against inline handler strings — it only stops creating one from a function. on:/oncapture:are rejected with a fix-it namingon-<exact>:on:click=fn→onClick=fn(oron-click=fnfor a custom name).- On a component, an
on*attribute is an ordinary prop (<Row onSelect=pick/>passes the callback), never an event.
Hooks and boundaries
import { useState } from "hono/jsx";
<const/state=useState(0)/>
<const/count=state[0]/>
<const/setCount=state[1]/>
<button onClick() { setCount(count + 1); }>${count}</button>
<const> is emitted in the component body, where hooks belong. static is
module scope and must not call hooks. Marko’s stateful tags are errors naming
Hono equivalents (useState, useEffect, useId).
<try> lowers to hono/jsx’s own ErrorBoundary, imported directly —
its fallback prop is fallbackRender, a function of the error, unlike
Preact’s/React’s fallback (which also accepts a bare node); <@placeholder>
uses hono/jsx’s own Suspense. Neither is wrapped by this package. With
both present the placeholder nests inside the boundary, so a render error
reaches the catch:
<try>
<p>${input.body()}</p>
<@placeholder><p>loading</p></@placeholder>
<@catch|err|><p>${err.message}</p></@catch>
</try>
<ErrorBoundary fallbackRender={(err) => <p>{err.message}</p>}>
<Suspense fallback={<p>loading</p>}>
<p>{input.body()}</p>
</Suspense>
</ErrorBoundary>
What each construct lowers to
Every structural kind becomes a plain JSX expression — Hono’s JSX has no control-flow components, so there is nothing else for them to become.
| Written | Emitted |
|---|---|
text, ${expr} |
text, {expr} |
$!{expr} as the sole child |
dangerouslySetInnerHTML |
class="a" |
class="a" (Hono takes class directly) |
class={a: cond} |
class={__mxClass({a: cond})} |
<label for=…> |
for={…} (native, unchanged) |
style={color: c} |
style={{color: c}} |
onClick() { … } |
onClick={() => { … }} |
<if> / <else-if> / <else> |
a conditional expression, null for a missing else |
<for|x| of=xs> |
{[...xs].map((x) => …)} with a key |
<for|v, k| in=obj> |
Object.entries map, keyed by the property name |
<for|i| from=a to=b> |
a generated range map |
<Comp x=1/> |
<Comp x={1}/> |
<Comp>children</Comp> |
children passed the JSX way |
<Comp|item|> |
a render-prop callback |
<@name> |
the prop name; repeated tags become an array |
<const/x=expr/> |
a const in the component body |
<define/Row|p|> |
a nested function component |
<try> |
ErrorBoundary / Suspense, both from hono/jsx |
Element-versus-component follows Marko’s rule, not JSX’s: a tag resolves
to a component when a binding or taglib entry says so, whatever its case. A
tags/-discovered <badge/> is a component call, not a literal <badge>
element.
Every <for> row carries a key
Hono re-creates rows without one, the same as Preact/React. by="id" names a
field of the row, by=fn is a function of it, and with no by= the key is
the row’s own identity — the item for of, the property name for in, the
loop value for a range.
That default is right for unique primitives and stable ranges. Two cases need
an explicit by=, neither detectable at compile time:
- Duplicates.
["a", "b", "a"]keys two rows"a"; the renderer may reconcile wrongly. Key by position:by=(tag, index) => index. - Objects. Identity changes whenever the array is rebuilt, remounting
every row. Key by a stable field:
by="id".
No client hydration by default
hono/jsx’s server render (String(jsx(Component, input)), or Hono’s own
c.html(...)) produces plain HTML with no resume markers, island wrappers, or
hydration bootstrap script. examples/hono-app’s e2e suite asserts the
response contains no <script> tag at all.
Bun loader
@mxlang/hono/bun registers a Bun plugin loading .mx/.marko files as
loader: "tsx" — the same shape as @mxlang/html/bun’s Bun loader, but
"tsx" instead of "ts" since this host’s compiled output contains JSX. Bun
honors the emitted /** @jsxImportSource hono/jsx */ pragma per file, so no
bundler is required for a plain Bun server. A hand-written .tsx sibling
without its own pragma still needs the running script’s own project
tsconfig.json to set jsxImportSource: "hono/jsx" — running it under a
different project’s tsconfig (or an override pointing elsewhere) makes Bun’s
JSX transform silently pick the wrong runtime for that one file. See
examples/hono-app.
Verification
bun run oracle:hono renders the 43 stock fixtures through hono/jsx
(String(jsx(Component, input)), awaited when the tree contains a caught
error — ErrorBoundary resolves asynchronously once its child throws): 30
pass, 13 reasoned skips, 0 bugs — identical to oracle:preact and
oracle:react, since all three targets share the emitter and the skip list.
examples/hono-app adds the server proof: Playwright fetches a live Hono
server rendering a <for> list and two <try> blocks, one of which the
built-in ErrorBoundary catches.