@myexamsai/conjure
v3.0.1
Published
Conjure — Type-safe server-side document builder for @myexamsai/revelio. Define components with Zod schemas, compose them into SduiLayoutDocuments with a fluent API, and push real-time patches — all fully typed.
Maintainers
Readme
@myexamsai/conjure
Type-safe server-side document builder for @myexamsai/revelio.
Conjure is the server half of the Revelio Server-Driven UI system. You use it to describe UI screens as structured data — called Blueprints — that are serialised to JSON and consumed by the Revelio client renderer. The key idea is that your server decides what the screen looks like; the client renders whatever Blueprint it receives without containing any screen-specific logic.
Table of Contents
Installation
npm install @myexamsai/conjure zodZod is a peer dependency. Conjure requires Zod v4.
Core Concepts
Blueprint
A Blueprint is a complete description of a screen — a JSON-serialisable tree of nodes with optional metadata, variables, and a design-token palette. Blueprints are produced by draft() and sent from your API to the Revelio client.
Blueprint
├── metadata (kind, sequenceNumber, id, name, …)
├── palette (colors, spacing, typography, borderRadius)
├── variables (global key-value pairs)
└── root (BlueprintNode — the top of the UI tree)
└── children[]
└── …BlueprintNode
Every element in the UI tree is a BlueprintNode. A node has:
id— unique within the Blueprint, used by Revelio to track identity across updatestype— maps to a registered component in Revelio'scomponentMapstate— the component's props; validated against its Zod schemaattributes— style hints and analytics declarations, passed through opaquelychildren— nested nodes (for container components)reference— IDs of other nodes whose state this node subscribes to
Structural vs Update Blueprints
draft() produces a structural Blueprint (metadata.kind = 'structural'). This is a full layout description that Revelio uses to replace or initialise a screen.
revision() produces an update Blueprint (metadata.kind = 'update'). It contains only the nodes that have changed. Revelio merges these surgically into the existing store without touching anything else.
Both functions auto-stamp a monotonically increasing sequenceNumber. Revelio uses this to discard out-of-order updates.
Shared Schemas
The Zod schema passed to define() is the single source of truth for a component's state. You share the same descriptor object between server and client — the server uses .node() to build typed nodes, and the client passes .schema to useSduiNodeSubscription for end-to-end type safety.
Quick Start
// shared/components.ts ← imported by BOTH server and client
import { define } from '@myexamsai/conjure'
import { z } from 'zod'
export const Card = define('Card', z.object({
title: z.string(),
subtitle: z.string().optional(),
}))
export const Button = define('Button', z.object({
label: z.string(),
variant: z.enum(['primary', 'secondary', 'ghost', 'danger']),
action: z.record(z.unknown()).optional(),
}))
export const Stat = define('Stat', z.object({
label: z.string(),
value: z.string(),
delta: z.string().optional(),
}))// server/routes/dashboard.ts
import { draft, revision, actions, blocks, beacon, asset, createPalette } from '@myexamsai/conjure'
import { Card, Button, Stat } from '../shared/components'
// Build a full screen
app.get('/api/sdui/dashboard', (req, res) => {
res.json(
draft({
metadata: { id: 'dashboard', name: 'Dashboard' },
palette: createPalette({ colors: { primary: '#6200EE' } }),
variables: { userId: req.user.id },
root: Card.node('root', { title: `Welcome, ${req.user.name}` }, {
children: [
blocks.row('stats', ['stat-revenue', 'stat-orders'], { gap: 12 }),
Stat.node('stat-revenue', { label: 'Revenue', value: '$48k', delta: '+12%' }),
Stat.node('stat-orders', { label: 'Orders', value: '142' }),
Button.node('cta', {
label: 'View all orders',
variant: 'primary',
action: actions.navigate('orders'),
}, {
attributes: {
analytics: [beacon.onClick('cta_tapped', { screen: 'dashboard' })],
},
}),
blocks.image('hero', asset.url('https://cdn.example.com/banner.png'), {
width: '100%',
}),
],
}),
})
)
})
// Push a real-time update over SSE
function pushRevenueUpdate(res: Response, newValue: string) {
const payload = JSON.stringify(
revision({
nodes: [
Stat.patch('stat-revenue', { value: newValue }),
],
})
)
res.write(`data: ${payload}\n\n`)
}API Reference
define()
function define<TShape, TSchema>(
type: string,
schema: ZodObject<TShape>
): ComponentDescriptor<TShape, TSchema>Defines a component by pairing a type name with a Zod schema. Returns a ComponentDescriptor that exposes .node() and .patch() factory methods.
Calling define() also registers the descriptor in an internal type registry. This registry is used by revision() to validate node state automatically — you do not need to pass a component map to revision().
Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| type | string | Component type name. Must match the key used in Revelio's componentMap. Case-sensitive. |
| schema | ZodObject | Zod object schema describing the component's state shape. |
Returns — ComponentDescriptor with the following members:
| Member | Description |
|--------|-------------|
| .type | The type name string (read-only). |
| .schema | The Zod schema (read-only). Share this with the client for useSduiNodeSubscription. |
| .node(id, state, opts?) | Creates a fully typed BlueprintNode. TypeScript enforces that state matches the schema. |
| .patch(id, partialState) | Creates a minimal node carrying only the changed fields, for use with revision(). |
ComponentDescriptor.node(id, state, opts?)
Card.node('hero', { title: 'Hello' })
Card.node('hero', { title: 'Hello' }, {
attributes: { style: { padding: 16 } },
children: [Button.node('cta', { label: 'Go', variant: 'primary' })],
reference: 'some-other-node-id',
})| Option | Type | Description |
|--------|------|-------------|
| attributes | Record<string, unknown> | Style hints, analytics declarations, or any opaque data passed to the component. |
| children | BlueprintNode[] | Child nodes nested under this node. |
| reference | string \| string[] | Node IDs this node subscribes to for state sharing. |
ComponentDescriptor.patch(id, partialState)
Stat.patch('revenue', { value: '$52k' })
// → { id: 'revenue', type: 'Stat', state: { value: '$52k' } }Produces a node carrying only the fields you pass. Use this as input to revision(). Only the provided state fields are merged on the client; unprovided fields are left unchanged.
Example
import { define } from '@myexamsai/conjure'
import { z } from 'zod'
export const ProductCard = define('ProductCard', z.object({
name: z.string(),
price: z.number(),
imageId: z.string().optional(),
badge: z.object({
label: z.string(),
variant: z.enum(['sale', 'new', 'sold-out']),
}).optional(),
}))
// TypeScript enforces state shape — price: 'free' would be a compile error
ProductCard.node('product-1', { name: 'Widget', price: 9.99 })draft()
function draft(options: BuildDocumentOptions): BlueprintAssembles a complete structural Blueprint ready to send to the Revelio client. Automatically stamps metadata.kind = 'structural' and an auto-incrementing metadata.sequenceNumber on every call.
BuildDocumentOptions
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| root | BlueprintNode | ✓ | The root node of the UI tree. |
| metadata | DraftMetadataInput | — | Document identity and display metadata. kind and sequenceNumber are excluded — they are stamped automatically. |
| variables | Record<string, unknown> | — | Global key-value pairs accessible to all nodes via the Revelio store. |
| version | string | — | Schema version string. Defaults to '1.0.0'. Increment on breaking layout changes so Revelio can detect genuine document replacements. |
| palette | ConjurePalette | — | Design-token palette. See createPalette(). |
DraftMetadataInput — all fields optional:
| Field | Type | Description |
|-------|------|-------------|
| id | string | Stable document identity. Revelio uses this to detect when a genuinely different screen has been loaded. |
| name | string | Human-readable screen name. |
| description | string | Optional description. |
| createdAt | string | ISO-8601 timestamp. |
| updatedAt | string | ISO-8601 timestamp. |
| author | string | Originating service or author. |
| generatedAt | string | ISO-8601 generation timestamp. |
| pageKey | string | Page / route key this Blueprint was generated for. |
Example
import { draft, createPalette } from '@myexamsai/conjure'
import { Screen, Card } from './components'
app.get('/api/sdui/profile/:userId', async (req, res) => {
const user = await db.users.find(req.params.userId)
res.json(
draft({
version: '2.0.0',
metadata: {
id: `profile:${user.id}`,
name: 'User Profile',
pageKey: 'profile',
},
variables: {
userId: user.id,
locale: req.headers['accept-language'] ?? 'en',
},
palette: createPalette({
colors: { primary: user.brandColor },
}),
root: Screen.node('root', { title: user.displayName }, {
children: [
Card.node('bio', { content: user.bio }),
],
}),
})
)
})revision()
function revision(input: RevisionInput): BlueprintBuilds a minimal update Blueprint containing only the nodes that have changed. Revelio merges these nodes into the live store without re-rendering the full screen.
Unlike draft(), a revision does not carry a full node tree, palette, or variables. It carries only the changed nodes, wrapped in a synthetic container (__revision__).
revision() validates each node's state against the Zod schema registered by define(). It throws RevisionValidationError on the first failure.
RevisionInput
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| nodes | BlueprintNode[] | ✓ | Nodes with updated state. Produce them via ComponentDescriptor.patch(). |
| sequenceNumber | number | — | Explicit sequence number. Omit to use the auto-incrementing shared counter. |
Throws — RevisionValidationError with .nodeId, .path, and .message if any node's state is invalid.
Example
import { revision } from '@myexamsai/conjure'
import { Stat, Card } from './components'
// Single node — write to an active SSE response
res.write(`data: ${JSON.stringify(
revision({
nodes: [Stat.patch('revenue', { value: '$52k', delta: '+8%' })],
})
)}\n\n`)
// Multiple nodes at once
res.write(`data: ${JSON.stringify(
revision({
nodes: [
Stat.patch('revenue', { value: '$52k' }),
Stat.patch('orders', { value: '148' }),
Card.patch('header', { subtitle: `Updated ${new Date().toLocaleTimeString()}` }),
],
})
)}\n\n`)
// With explicit sequence number (e.g. from an event stream)
revision({ nodes: [...], sequenceNumber: event.seq })Validation behaviour
revision() only validates nodes whose type was registered via define(). Nodes with unrecognised types (e.g. block:* nodes) are passed through without validation. This matches the full-document behaviour of assertDocument().
blocks
import { blocks } from '@myexamsai/conjure'Built-in layout and content primitives. Nodes in the blocks namespace use a block: type prefix and are rendered natively by Revelio — no componentMap registration is required.
All five builders accept an optional BlockStyle object as their last argument.
blocks.stack(id, childrenIds, style?)
A generic stacking container. Children are stacked in the platform's default direction.
blocks.stack(id: string, childrenIds: string[], style?: BlockStyle): BlueprintNode
// → { id, type: 'block:stack', state: { childrenIds }, attributes: { style } }blocks.stack('feed', ['post-1', 'post-2', 'post-3'], { gap: 8 })blocks.row(id, childrenIds, style?)
A horizontal container. Children are laid out left-to-right.
blocks.row(id: string, childrenIds: string[], style?: BlockStyle): BlueprintNode
// → { id, type: 'block:row', state: { childrenIds }, attributes: { style } }blocks.row('toolbar', ['back-btn', 'title', 'menu-btn'], {
justify: 'between',
align: 'center',
padding: 12,
})blocks.column(id, childrenIds, style?)
A vertical container. Children are laid out top-to-bottom.
blocks.column(id: stri