# @inlay/render Server-side component resolution for Inlay element trees. Given an element like ``, the renderer looks up the component implementation, expands it, and recurses until everything is primitives the host can render. ## Install ``` npm install @inlay/render @inlay/core ``` ## Example A minimal Hono server that accepts an element, resolves it through packs, and returns HTML. Uses Hono's JSX for safe output. ```tsx /** @jsxImportSource hono/jsx */ import { Hono } from "hono"; import { serve } from "@hono/node-server"; import { $, serializeTree, deserializeTree, isValidElement } from "@inlay/core"; import { render } from "@inlay/render"; import type { RenderContext } from "@inlay/render"; // Records — in-memory stand-in for AT Protocol const records = { // Primitives: no body. The host renders these directly. "at://did:plc:host/at.inlay.component/stack": { $type: "at.inlay.component", type: "org.atsui.Stack", }, "at://did:plc:host/at.inlay.component/text": { $type: "at.inlay.component", type: "org.atsui.Text", }, // Template: a stored element tree with Binding placeholders. "at://did:plc:app/at.inlay.component/greeting": { $type: "at.inlay.component", type: "com.example.Greeting", body: { $type: "at.inlay.component#bodyTemplate", node: serializeTree( $("org.atsui.Stack", { gap: "small" }, $("org.atsui.Text", {}, "Hello, "), $("org.atsui.Text", {}, $("at.inlay.Binding", { path: ["name"] })), ) ), }, imports: ["at://did:plc:host/at.inlay.pack/main"], }, // Pack: maps NSIDs → component records "at://did:plc:host/at.inlay.pack/main": { $type: "at.inlay.pack", name: "host", exports: [ { type: "org.atsui.Stack", component: "at://did:plc:host/at.inlay.component/stack" }, { type: "org.atsui.Text", component: "at://did:plc:host/at.inlay.component/text" }, { type: "com.example.Greeting", component: "at://did:plc:app/at.inlay.component/greeting" }, ], }, }; // Resolver — I/O layer that render() calls into const resolver = { async fetchRecord(uri) { return records[uri] ?? null; }, async xrpc(params) { throw new Error(`No handler for ${params.nsid}`); }, async resolveLexicon() { return null; }, }; // Primitives → JSX const primitives = { "org.atsui.Stack": async ({ props, ctx }) => { const p = props as { gap?: string; children?: unknown[] }; return (
{await renderChildren(p.children, ctx)}
); }, "org.atsui.Text": async ({ props, ctx }) => { const p = props as { children?: unknown }; return {await renderNode(p.children, ctx)}; }, }; // Walk loop — resolve elements, render primitives to JSX async function renderNode(node, ctx) { if (node == null) return <>; if (typeof node === "string") return <>{node}; if (Array.isArray(node)) return <>{await Promise.all(node.map(n => renderNode(n, ctx)))}; if (isValidElement(node)) { const result = await render(node, ctx, { resolver }); if (result.node === null) { // Primitive: host renders using element type + result.props const Primitive = primitives[node.type]; if (!Primitive) return <>{``}; return Primitive({ props: result.props, ctx: result.context }); } return renderNode(result.node, result.context); } return <>{String(node)}; } async function renderChildren(children, ctx) { if (!children) return <>; const items = Array.isArray(children) ? children : [children]; return <>{await Promise.all(items.map(c => renderNode(c, ctx)))}; } // Server const app = new Hono(); app.get("/", async (c) => { const element = deserializeTree({ $: "$", type: "com.example.Greeting", props: { name: "world" }, }); const ctx = { imports: ["at://did:plc:host/at.inlay.pack/main"] }; const body = await renderNode(element, ctx); return c.html({body}); }); serve({ fetch: app.fetch, port: 3000 }); ``` Returns: ```html
Hello, world
``` The `com.example.Greeting` template expanded its `Binding` for `name`, resolved to `org.atsui.Stack` and `org.atsui.Text` primitives, and the host turned those into HTML. ## In practice A production host will also want: - **Persistent global caching** keyed by component + props + response cache tags - **Streaming** so slow XRPC calls and record fetches don't block the page - **Prefetching** for links so navigation feels instant See [`proto/`](../../../proto) for a working implementation with all of the above. ## How it works `render()` resolves one element at a time. It returns `node: null` when the element is a primitive (the host renders it using `element.type` + `result.props`) or a non-null node tree when the element expanded into a subtree that needs more passes. The walk loop drives this to completion. 1. **Binding resolution** — `at.inlay.Binding` elements in props are resolved against the current scope. The concrete values are available on `result.props`. 2. **Type lookup** — the element's NSID is looked up across the import stack. First pack that exports the type wins. 3. **Component rendering** — depends on the component's body: - **No body** — a primitive; returns `node: null` with resolved props. - **Template** — element tree expanded with prop bindings. - **External** — XRPC call to the component's service. Children are passed as slots and restored from the response. 4. **Error handling** — errors throw with `componentStack` stamped on the error. `MissingError` propagates for `at.inlay.Maybe` to catch. ## API | Function | Description | |----------|-------------| | `createContext(component, componentUri?)` | Create a `RenderContext` from a component record | | `render(element, context, options)` | Render a single element. Returns `Promise` | | `resolvePath(obj, path)` | Resolve a dotted path array against an object | | `MissingError` | Thrown when a binding path can't be resolved | **Render options:** - `resolver: Resolver` — I/O callbacks (required) - `maxDepth?: number` — nesting limit (default 30) ### Types - **`Resolver`** — `{ fetchRecord, xrpc, resolveLexicon }` - **`RenderContext`** — `{ imports, component?, componentUri?, depth?, scope?, stack? }` - **`RenderResult`** — `{ node, context, props, cache? }` - **`RenderOptions`** — `{ resolver, maxDepth? }` - **`ComponentRecord`** — re-exported from generated lexicon defs - **`CachePolicy`** — re-exported from generated lexicon defs