Skip to content

Literal cross-package JSX

Servable’s own <Route>/<Group> can be written literally, nested directly inside <Gateway>/<Host>, in the same file, under one @jsxImportSource @johnhenry/hostable pragma:

/** @jsxImportSource @johnhenry/hostable */
import { Gateway, Host, Group, Route, Upstream, compile } from "@johnhenry/hostable";
const app = (
<Gateway>
<Host name="a.example.com">
<Group prefix="/api">
<Route path="/users" method="GET">{{ users: [] }}</Route>
</Group>
<Upstream path="/*" handler={async () => new Response("fallback")} />
</Host>
</Gateway>
);

This works because hostable’s jsx() calls any function-typed tag directly with its props (type(allProps)) rather than treating it as markup, and Descriptor.tag’s type is the general symbol (not hostable’s own exact typeof FRAGMENT) specifically so a sibling package’s differently-keyed Fragment marker still type-checks.

The same mechanism goes a layer deeper: fileable’s <Dir>/<File> can be nested directly inside servable’s <Group>, which is itself nested inside hostable’s <Host> — three packages’ JSX, one pragma, one expression:

/** @jsxImportSource @johnhenry/hostable */
import { Dir, File } from "@johnhenry/fileable";
import { Gateway, Host, Group, Route, compile } from "@johnhenry/hostable";
const app = (
<Gateway>
<Host name="a.example.com">
<Group prefix="/static">
{/* "dist" IS part of the URL -- this file serves at
https://a.example.com/static/dist/index.html. See servable's
"Mounting a fileable tree" for the full naming rule (including
the Fragment-based way to mount without a folder name at all,
and mounting a bare <File> -- named or not -- with no <Dir>
wrapper). */}
<Dir name="dist">
<File name="index.html">{"<h1>Home</h1>"}</File>
</Dir>
</Group>
<Route path="/api/hello" method="GET">{{ hello: "world" }}</Route>
</Host>
</Gateway>
);

See examples/05-nested-jsx in the repo for the full, verified version (static assets, a JSON API, and a reverse-proxied second domain, all in one tree).

Fileable descriptors are recognized the same way here as in plain servable — as a child of <Group>/<Router> (or that Group’s own from=), never as a child of <Route>/<Upstream>. Putting one under <Route> throws the same clear compile-time error servable’s own compile stage throws (hostable delegates to servable’s compile() for everything below <Host>, so this isn’t reimplemented here) — use <Route>’s own body/src= handling for a single file’s content at one route instead.

<>...</> works directly under <Host>/<Gateway> here, the same as it does under servable’s own <Router>/<Group>, and the same as fileable’s own <>...</> works as a mount root. All three compose in one nested tree without conflicting:

/** @jsxImportSource @johnhenry/hostable */
import { Dir, File } from "@johnhenry/fileable";
import { Gateway, Host, Group, Route, compile } from "@johnhenry/hostable";
// A fileable Fragment is genuinely necessary here: Group's `from=` only
// ever accepts ONE value, so mounting multiple named files with no
// enclosing folder name in the URL requires bundling them into that one
// slot.
const site = (
<>
<File name="index.html">{"home"}</File>
<File name="about.html">{"about"}</File>
</>
);
// A component function returning a Fragment is the OTHER place Fragment
// earns its keep: a function can only return one value, and Fragment is
// what lets that one value stand for several sibling Routes -- reusable
// across as many Hosts as you embed it in, not copy-pasted per domain.
function CommonRoutes() {
return (
<>
<Route path="/health" method="GET">{{ ok: true }}</Route>
<Route path="/version" method="GET">{{ version: "1.0.0" }}</Route>
</>
);
}
const app = (
<Gateway>
<Host name="a.example.com">
<Group prefix="/static">{site}</Group>
<CommonRoutes />
</Host>
<Host name="b.example.com">
<CommonRoutes />
</Host>
</Gateway>
);

<CommonRoutes />, capitalized — not <commonRoutes />. This isn’t specific to this package: every JSX transform decides how to compile a tag name from its capitalization alone, before any custom jsx() factory ever runs — a lowercase-starting tag always compiles to a bare string (jsx("commonRoutes", {})), which this family’s jsx() then treats as an unrecognized markup tag and rejects (<commonRoutes> is not a servable primitive here); an uppercase-starting tag compiles to a lookup of the CommonRoutes binding (jsx(CommonRoutes, {})), which jsx() already has a branch for — typeof type === "function" — and calls directly. Same convention this whole ecosystem uses for Dir/File/Route/Group/ Host themselves.

hostable’s own exported Fragment (from @johnhenry/hostable/jsx-runtime, what <>...</> compiles to under this package’s @jsxImportSource) is deliberately servable’s Fragment symbol, re-exported rather than redefined — hostable has no build/resolve/layout pipeline of its own, it hands everything to servable’s real compile(), so a genuinely distinct hostable Fragment symbol would need servable’s own build() to recognize it too, which it never would (each layer’s Fragment/FILEABLE_DESCRIPTOR detection is scoped to its own, Symbol.for()-registry-keyed marker).

An earlier version of this package did mint its own distinct Symbol.for("hostable.fragment") — a real bug, caught by actually compiling and fetching a <>...</> under this pragma rather than just reasoning about the symbol keys statically: it threw <Symbol(hostable.fragment)> is not a servable primitive here the moment it was used for anything beyond being re-exported. Fixed; see test/fragment.test.tsx in the repo for the regression coverage (both the standalone case and the fileable/hostable-Fragments-composed-together case above).