Skip to content

Latest commit

 

History

100 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HSX is love, HSX is life

First things, first... What the hack does HSX stand for? I'll say it's 'HTML for Server-Side eXtensions'. :0

But, honestly, I prefer: HTMX Slaps Xtremely :)

SSR-only JSX/TSX renderer for Deno that hides HTMX-style attributes away during the rendering process, and compiles them to hx-* attributes.

Disclaimer: this was a quick hack in my free time, held together by vibe coding and espresso. I like it a lot, but consider it an early release. I feel it is getting better (a lot)

TL;DR: Like JSX, but for SSR HTML + HTMX.

Features

  • SSR-only rendering - Outputs HTML; the optional client bus handles SSE and application events when a page uses them.
  • HTMX as HTML - Write get, post, target, swap as native attributes
  • Type-safe routes - Branded Route<Path> types with automatic parameter inference
  • Co-located components - hsxComponent() bundles route + handler + render
  • Page guardrails - hsxPage() enforces semantic, style-free layouts
  • Branded IDs - id("name") returns Id<"name"> typed as "#name"
  • Auto HTMX injection - <script src="/static/htmx.js"> injected when needed
  • No manual hx-* - Throws at render time if you write hx-get directly
  • Widgets - Define once, serve via SSR or embed as iframes with Declarative Shadow DOM
  • Generative UI - AI models select and render widgets via tool calling, streamed to the browser via SSE + HTMX
  • Agent-operable - Any hsxComponent that declares describe + input becomes an AI tool; an agent drives your real endpoints from the same definition that serves humans
  • HSX Lens - Opt-in dev workbench that maps page samples, components, widgets, targets, and agent tools into a hypermedia manifest

Installation

From JSR

deno add jsr:@srdjan/hsx

Or import directly:

import { id, render, route } from "jsr:@srdjan/hsx";

Separate Packages

HSX is a monorepo with seven packages:

// Core - JSX rendering, type-safe routes, hsxComponent, hsxPage, SSE
import {
  Fragment,
  hsxComponent,
  hsxPage,
  id,
  render,
  renderSSE,
  route,
} from "jsr:@srdjan/hsx";

// Styles - ready-to-use CSS with theming support
import { HSX_STYLES_PATH, hsxStyles } from "jsr:@srdjan/hsx-styles";

// Widgets - embeddable widget protocol + SSR/embed adapters + GenUI catalog
import { widgetToHsxComponent } from "jsr:@srdjan/hsx-widgets/ssr";
import { createCatalog, type GenUIWidget } from "jsr:@srdjan/hsx-widgets";

// GenUI - AI-powered generative UI with tool calling
import {
  createGenUIHandler,
  createGenUIRoutes,
  createInMemoryConversationRepository,
} from "jsr:@srdjan/hsx-genui";
import { claudeProvider } from "jsr:@srdjan/hsx-genui/claude";

// Agent - make your app agent-operable (components become AI tools)
import { createAppAgent } from "jsr:@srdjan/hsx-agent";

// MCP - serve those components to external MCP clients (Claude Code, ...)
import { createMcpHandler } from "jsr:@srdjan/hsx-mcp";

// Lens - inspect the app's hypermedia contract during development
import { createHsxLens } from "jsr:@srdjan/hsx-lens";

Install individually:

deno add jsr:@srdjan/hsx
deno add jsr:@srdjan/hsx-styles
deno add jsr:@srdjan/hsx-widgets
deno add jsr:@srdjan/hsx-genui
deno add jsr:@srdjan/hsx-agent
deno add jsr:@srdjan/hsx-mcp
deno add jsr:@srdjan/hsx-lens

Selective Imports (Tree-Shaking)

For smaller bundles, import only what you need:

// Core only - render, route, id, Fragment (smaller bundle)
import { Fragment, id, render, route } from "jsr:@srdjan/hsx/core";

// Components only - hsxComponent, hsxPage
import { hsxComponent, hsxPage } from "jsr:@srdjan/hsx/components";

// Everything (default)
import { hsxComponent, hsxPage, render, route } from "jsr:@srdjan/hsx";

From Source

Clone and import using workspace package names:

import { hsxComponent, hsxPage, id, render, route } from "@srdjan/hsx";

Quick Start (Low-Level API)

Note: This shows the low-level API using route(). For most projects, use the hsxComponent pattern below instead.

import { id, render, route } from "@srdjan/hsx";

const routes = {
  todos: route("/todos"),
};

const ids = {
  list: id("todo-list"),
};

function Page() {
  return (
    <html>
      <head>
        <title>HSX Demo</title>
      </head>
      <body>
        <form post={routes.todos.bind()} target={ids.list} swap="outerHTML">
          <input name="text" required />
          <button type="submit">Add</button>
        </form>
        <ul id="todo-list">{/* items */}</ul>
      </body>
    </html>
  );
}

Deno.serve(() => render(<Page />));

Output HTML:

<html>
  <head>
    <title>HSX Demo</title>
  </head>
  <body>
    <form hx-post="/todos" hx-target="#todo-list" hx-swap="outerHTML">
      <input name="text" required>
      <button type="submit">Add</button>
    </form>
    <ul id="todo-list"></ul>
    <script src="/static/htmx.js"></script>
  </body>
</html>

HSX Component Pattern (Recommended)

import { hsxComponent } from "jsr:@srdjan/hsx";

export const TodoList = hsxComponent("/todos", {
  methods: ["GET", "POST"],

  async handler(req) {
    if (req.method === "POST") {
      const form = await req.formData();
      await addTodo(String(form.get("text")));
    }
    return { todos: await getTodos() }; // must match render props
  },

  render({ todos }) {
    return (
      <ul id="todo-list">
        {todos.map((t) => <li key={t.id}>{t.text}</li>)}
      </ul>
    );
  },
});

// Use as route in JSX
<form post={TodoList.bind()} target="#todo-list" swap="outerHTML" />;

// Use as handler in your server
if (TodoList.match(url.pathname)) return TodoList.handle(req);

TypeScript enforces that handler returns the same shape that render expects. methods defaults to ["GET"]. Component handlers return the rendered tree with an HTML content type, whether that tree is a fragment or a full document.

Page.Component returns a PageRenderable. Rendering <Page.Component /> through render() or renderHtml() therefore applies the same page validation and script-injection policy as Page.render().

Choose one style: Use either hsxComponent (recommended) or the low-level route() API, but don't mix them in the same project.

hsxPage (full-page guardrails)

hsxPage() wraps a render function that returns a complete HTML document and validates that:

  • The root is <html> with <head> then <body>
  • Semantic tags (header/main/section/article/h1-h6/p/ul/ol/li/etc.) have no class or inline style
  • <style> tags live in <head>; CSS belongs there, not inline
  • Composition stays within semantic HTML + HSX components
import { hsxComponent, hsxPage } from "jsr:@srdjan/hsx";

const Widget = hsxComponent("/data", {
  handler: () => ({ message: "Hi" }),
  render: ({ message }) => <p>{message}</p>,
});

const Page = hsxPage(() => (
  <html lang="en">
    <head>
      <title>Guarded Page</title>
      <style>{"body { font-family: system-ui; }"}</style>
    </head>
    <body>
      <header>
        <h1>Welcome</h1>
      </header>
      <main>
        <section>
          <div data-surface="card">
            <Widget.Component />
          </div>
        </section>
      </main>
    </body>
  </html>
));

Deno.serve(() => Page.render());

HSX Attributes

HSX Attribute Renders To Description
get hx-get HTTP GET request
post hx-post HTTP POST request
put hx-put HTTP PUT request
patch hx-patch HTTP PATCH request
delete hx-delete HTTP DELETE request
query hx-query HTTP QUERY request
target hx-target Element to update
swap hx-swap How to swap content
trigger hx-trigger Event that triggers request
vals hx-vals Additional values (JSON)
headers hx-headers Custom headers (JSON)
behavior="boost" hx-boost="true" Enable boost mode (<a> only)
include hx-include Extra values to send
statusSwap hx-status:CODE Per-status swap overrides
targetInherited hx-target:inherited Pass a target to descendants

HTMX 4 defaults implicitInheritance to false, so a parent's value only reaches its descendants through an Inherited alias. The full table, including select, indicator, sync, confirm, preserve, encoding, validate, config, and the morph and history attributes, is in docs/HTMX_INTEGRATION.md.

Supported elements: form, button, a, div, span, section, article, ul, tbody, tr

Type-Safe Routes

Use route() to create type-safe routes with automatic parameter extraction:

const routes = {
  users: {
    list: route("/users"),
    detail: route("/users/:id"),
    posts: route("/users/:userId/posts/:postId"),
  },
};

// Bind before JSX. Path parameters are type-checked and URL-encoded:
<button get={routes.users.detail.bind({ id: 42 })}>View</button>;
// Renders: <button hx-get="/users/42">View</button>

Branded IDs

Use id() to create type-safe element references:

const ids = {
  list: id("todo-list"),    // Type: Id<"todo-list"> = "#todo-list"
  count: id("item-count"),
};

// In JSX:
<ul id="todo-list">...</ul>
<button get="/todos" target={ids.list} swap="innerHTML">Refresh</button>
// Renders: <button hx-get="/todos" hx-target="#todo-list" hx-swap="innerHTML">

Wrapper Components

Create reusable wrapper components that pass through HSX attributes for cleaner JSX:

import type { HsxSwap, Urlish } from "jsr:@srdjan/hsx";

function Card(props: {
  children: unknown;
  title?: string;
  get?: Urlish;
  trigger?: string;
  swap?: HsxSwap;
}) {
  return (
    <div
      data-surface="card"
      data-layout="stack"
      data-gap="4"
      get={props.get}
      trigger={props.trigger}
      swap={props.swap}
    >
      {props.title && <h2>{props.title}</h2>}
      {props.children}
    </div>
  );
}

function Subtitle(props: { children: string }) {
  return (
    <div data-ui="prose">
      <p>{props.children}</p>
    </div>
  );
}

Usage:

function Page() {
  return (
    <main>
      <h1>Dashboard</h1>
      <Subtitle>Content loads lazily</Subtitle>
      <Card
        get={routes.stats.bind()}
        trigger="load"
        swap="innerHTML"
        title="Statistics"
      >
        <LoadingSkeleton />
      </Card>
      <Card title="Team Members">
        <UserList />
      </Card>
    </main>
  );
}

This pattern keeps your page components clean while maintaining full access to HSX attributes. See the examples/*/components.tsx files for more examples.

Configuration

Add this to your deno.json:

{
  "imports": {
    "hsx/jsx-runtime": "jsr:@srdjan/hsx/jsx-runtime"
  },
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "hsx"
  }
}

JSX intrinsic element types (<div>, <form>, <button>, etc.) are automatically included via the jsx-runtime import, with no additional type configuration needed.

Serving HTMX

HSX injects <script src="/static/htmx.js"> when HTMX is used. You must serve it:

if (url.pathname === "/static/htmx.js") {
  const js = await Deno.readTextFile("./vendor/htmx/htmx.js");
  return new Response(js, {
    headers: { "content-type": "text/javascript; charset=utf-8" },
  });
}

Optional Styles Module

HSX includes an optional Auras-based CSS module with a single bundled stylesheet:

import { HSX_STYLES_PATH, hsxStyles } from "jsr:@srdjan/hsx-styles";

// Serve the styles
if (url.pathname === HSX_STYLES_PATH) {
  return new Response(hsxStyles, {
    headers: { "content-type": "text/css; charset=utf-8" },
  });
}

// In your page head
<link rel="stylesheet" href={HSX_STYLES_PATH} />;

Exports:

  • hsxStyles - Auras Elements + HSX brand layer
  • HSX_STYLES_PATH - Default path: /static/hsx.css

The package imports both CSS files as text modules. Importing @srdjan/hsx-styles does not require Deno filesystem read permission.

What you get:

  • Layout via data attributes: data-layout="row|col|stack|cluster|grid|container", data-gap, data-align, data-justify, data-grid-min
  • Surfaces via attributes: data-surface="card" and data-surface="notice"
  • Button variants via data-variant="solid|soft|ghost"
  • Theme controls via data-theme="dark", data-contrast="more", and data-motion="reduce"

Customization: Override CSS variables in your page:

<style>
  {`:root { --primary: #10b981; --bg: #f0fdf4; --border: #a7f3d0; }`}
</style>;

Core tokens include --primary, --primary-hover, --primary-subtle, --bg, --surface, --surface-raised, --border, --text, --text-muted, spacing (--space-*), radius (--radius-*), typography (--font-display, --text-*, --leading-*), shadows (--shadow-*), and layout tokens such as --container-max.

To force dark mode for a specific page, add data-theme="dark" to <html>.

HSX Widgets

The @srdjan/hsx-widgets package provides a widget protocol for building components that work in two contexts: SSR through HSX routes, and embeddable iframe shells for third-party pages.

Define a Widget

A widget is a typed record with validation, styles, rendering, and optional data loading:

import type { Widget } from "jsr:@srdjan/hsx-widgets";
import { fail, ok } from "jsr:@srdjan/hsx-widgets";

export const greetingWidget: Widget<GreetingProps> = {
  tag: "hsx-greeting",
  props: { validate(raw) {/* ... */} },
  styles: `.hsx-greeting { padding: 1rem; }`,
  render: (props) => (
    <div class="hsx-greeting">
      <h2>{props.name}</h2>
    </div>
  ),
  load: async (params) => ok({ name: params.name, message: `Hello!` }),
  shadow: "open", // Optional: Declarative Shadow DOM
};

Serve via SSR

Use widgetToHsxComponent() to bridge a widget into an HSX route:

import { widgetToHsxComponent } from "jsr:@srdjan/hsx-widgets/ssr";

const GreetingRoute = widgetToHsxComponent(greetingWidget, {
  path: "/widgets/greeting/:name",
});

if (GreetingRoute.match(url.pathname)) return GreetingRoute.handle(req);

Embed on Third-Party Pages

Serve iframe shells with createEmbedHandler(), then embed with a snippet:

<div data-hsx-uri="https://yoursite.com/embed/hsx-greeting?name=World"></div>
<script src="https://yoursite.com/static/hsx/snippet.js"></script>

See docs/WIDGETS.md for the full widget guide including Declarative Shadow DOM, style hoisting, and the build pipeline.

Generative UI

The @srdjan/hsx-genui package lets AI models render interactive widgets directly in a chat interface. The AI selects from a catalog of pre-registered widgets via tool calling, and the HSX client bus streams the rendered HTML to the browser over SSE.

Define GenUI Widgets

A GenUI widget extends the regular Widget protocol with AI metadata - a description, JSON Schema for props, and optional few-shot examples:

import type { GenUIWidget } from "jsr:@srdjan/hsx-widgets";
import { ok } from "jsr:@srdjan/hsx-widgets";

const weatherWidget: GenUIWidget<WeatherProps> = {
  tag: "hsx-weather",
  description:
    "Shows current weather for a city with temperature and conditions",
  schema: {
    type: "object",
    properties: {
      city: { type: "string", description: "City name" },
    },
    required: ["city"],
  },
  category: "display",
  props: {
    validate(raw) {
      /* ... */ return ok({ city: raw.city });
    },
  },
  styles: `.weather { padding: 1rem; }`,
  render: ({ city, temp }) => (
    <div class="weather">
      <h3>{city}</h3>
      <p>{temp}</p>
    </div>
  ),
  load: async (params) => {
    const data = await fetchWeather(params.city);
    return ok(data);
  },
};

Create a Catalog and Handler

Register widgets in a catalog, then wire up the GenUI handler with an AI provider:

import { createCatalog } from "jsr:@srdjan/hsx-widgets";
import {
  createGenUIHandler,
  createGenUIRoutes,
  createInMemoryConversationRepository,
} from "jsr:@srdjan/hsx-genui";
import { claudeProvider } from "jsr:@srdjan/hsx-genui/claude";

// 1. Register widgets
const catalog = createCatalog([weatherWidget, chartWidget]);

// 2. Create handler with Claude provider
const handler = createGenUIHandler({
  catalog,
  provider: claudeProvider({ model: "claude-sonnet-4-6" }),
});

// 3. Create routes (page shell + POST endpoint + SSE stream)
const repository = createInMemoryConversationRepository();
const { page, send, stream } = createGenUIRoutes({ handler, repository });

// 4. Serve
Deno.serve((req) => {
  const { pathname } = new URL(req.url);
  if (page.match(pathname)) return page.handle(req);
  if (send.match(pathname)) return send.handle(req);
  if (stream.match(pathname)) return stream.handle(req);
  return new Response("Not Found", { status: 404 });
});

The user types a message, the AI selects a registered widget tool, HSX renders it server-side, and the result streams into the page via SSE. Catalogs expose only registered widgets. They do not include a raw-content tool.

Design Guidelines

Include AI-readable design constraints in the system prompt:

import { createDesignGuidelines, formatForAI } from "jsr:@srdjan/hsx-widgets";

const guidelines = createDesignGuidelines({
  colors: "Use indigo accent. All colors via CSS custom properties.",
});

const handler = createGenUIHandler({
  catalog,
  provider,
  systemPrompt: formatForAI(guidelines),
  includeGuidelines: false, // we provided our own
});

SSE Streaming

The core @srdjan/hsx package also exports renderSSE() for building custom SSE endpoints from any async iterable of JSX:

import { renderSSE } from "jsr:@srdjan/hsx/core";

async function* generateWidgets() {
  yield <div>Loading...</div>;
  const data = await fetchData();
  yield <Chart data={data} />;
}

Deno.serve(() => renderSSE(generateWidgets()));

HTMX 4 removed SSE from core, so the HSX client bus drives it. The bus opens the EventSource and inserts each frame at the position named by swap:

<div sseConnect="/stream" sseSwap="message" swap="beforeend">
  {/* widgets appear here as SSE events arrive */}
</div>;

Serve the bus at /static/hsx-bus.js next to HTMX. renderSSE ends every stream with an hsx-sse-close frame, which the bus needs: a browser EventSource reconnects whenever the server closes the stream.

For hand-written streams, encodeSSEFrame({ event, data, id?, retry? }) accepts one frame object. It rejects invalid event names, IDs, and retry values.

Agent-Operable Apps

The @srdjan/hsx-agent package turns your existing application into an agent action space. Where GenUI lets an AI render display widgets, this lets an AI drive your real endpoints: each hsxComponent that declares describe + input becomes an AI tool from the same definition that serves humans over HTMX. The agent calls the component's own handle() and reads back the exact semantic HTML a person would see. One definition, two consumers.

A component is agent-callable only when it declares both describe and input. Components without them are invisible to the agent - no surprise mutations.

import { createAppAgent, toRequest } from "@srdjan/hsx-agent";
import type { AgentToolInvoker } from "@srdjan/hsx-agent";
import { claudeProvider } from "@srdjan/hsx-genui/claude";

// The same component the human form posts to is now also an AI tool:
const AddTodo = hsxComponent("/todos", {
  methods: ["POST"],
  describe: "Add a new todo item to the list.",
  input: {
    schema: {
      type: "object",
      properties: { text: { type: "string", description: "The todo text." } },
      required: ["text"],
    },
  },
  async handler(req) {
    const form = await req.formData();
    addTodo(String(form.get("text")));
    return {};
  },
  render: () => <TodoListView />,
});

// Humans use it via HTMX (unchanged):
<form post={AddTodo.bind()} swap="none" />;

// The agent uses the same definition as a tool:
const invokeTool: AgentToolInvoker = ({ args, component, sourceRequest }) =>
  component.handle(toRequest(component, args, sourceRequest));

const agent = createAppAgent({
  components: [AddTodo, ToggleTodo, ClearDone],
  provider: claudeProvider({ model: "claude-sonnet-4-6" }),
  invokeTool,
});

The shared agent registry compiles every declared input schema with Ajv's JSON Schema 2020-12 validator. Schemas without $schema use 2020-12. Invalid schemas fail when the registry is created, and invalid arguments never reach invokeTool.

invokeTool is required. The application owns authorization and execution at this boundary and receives the validated arguments, component, and originating request. The simple adapter above preserves the component HTTP path; production applications can enforce tenant, session, or capability policy first.

AppAgent is structurally a GenUI handler ({ handleTurn, tools }), so it drops straight into the pre-built chat routes:

import {
  createGenUIRoutes,
  createInMemoryConversationRepository,
} from "@srdjan/hsx-genui";

const repository = createInMemoryConversationRepository();
const { send, stream } = createGenUIRoutes({
  handler: agent,
  repository,
  basePath: "/copilot",
});

handleTurn({ turnId, message, history, sourceRequest }) emits rendered frames, transcript messages, and one terminal event. The repository owns branded conversation and turn IDs, atomic turn claims, immutable snapshots, and bounded frame replay. Reconnecting with Last-Event-ID resumes after the acknowledged frame without running the tool effects again. The send route queues execution, so a missing stream request cannot block later turns. Transient claim failures retry three times by default; claimRetry can inject the wait capability or adjust the bounded attempt count. Exhaustion atomically fails the pending turn with safe error and close frames instead of blocking the conversation. A cursor older than the retained replay window receives HTTP 409 instead of an incomplete suffix; malformed cursors and cursors beyond the stored turn receive HTTP 400. If a resumable browser connection closes instead of retrying, the bus marks the stream as failed and renders a generic refresh-to-recover message.

The agent's tool calls invoke the real components; each rendered fragment is streamed to the browser and fed back to the model as its observation. In the todos-copilot example the mutating components render the list with swapOob, so whether a change comes from the human form or the agent, the canonical #todo-list updates out-of-band while the agent narrates in the chat. Run it with ANTHROPIC_API_KEY=... deno task example:todos-copilot.

createAgentRegistry(components) and toRequest(component, args, sourceRequest) are exported for custom integrations that need the same schema validation and request construction.

MCP Server

The @srdjan/hsx-mcp package mounts a Model Context Protocol endpoint into your existing Deno.serve, so external MCP clients - Claude Code, Claude Desktop, any MCP-capable tool - can discover and operate the same agent-callable components. The in-app copilot needs a chat UI and an API key; the MCP endpoint needs neither. Write a component once and it serves humans over HTMX, your embedded copilot over SSE, and every MCP client over JSON-RPC, all through the component's own handle().

import { toRequest } from "@srdjan/hsx-agent";
import type { AgentToolInvoker } from "@srdjan/hsx-agent";
import { createMcpHandler } from "@srdjan/hsx-mcp";

const invokeTool: AgentToolInvoker = ({ args, component, sourceRequest }) =>
  component.handle(toRequest(component, args, sourceRequest));

const mcpToken = Deno.env.get("MCP_TOKEN");
if (!mcpToken) throw new Error("MCP_TOKEN is required");

const mcp = createMcpHandler({
  components: todoComponents,
  invokeTool,
  serverName: "todos",
  bearerToken: mcpToken,
});

Deno.serve((req) => {
  const mcpResponse = mcp.handle(req);
  if (mcpResponse) return mcpResponse;

  // Your app routes...
  return new Response("Not found", { status: 404 });
});

Connect from Claude Code:

claude mcp add --transport http todos http://localhost:8000/mcp

Tool results carry the HTTP status plus the same rendered HTML a human receives, so the agent observes the app as a user sees it. Pass a lens manifest (createHsxManifest() output) as manifest to expose the app's full hypermedia contract as MCP resource hsx://manifest.

The endpoint is stateless Streamable HTTP: POST-only, plain JSON responses, no sessions, and no SDK dependency. It accepts protocol versions 2025-11-25, 2025-06-18, and 2025-03-26. A missing MCP-Protocol-Version header defaults to 2025-03-26; malformed or unsupported values return HTTP 400 before JSON-RPC dispatch or tool effects. Initialize negotiation still selects the requested supported version or the newest supported version.

Security: MCP tools mutate real application state. Never mount the endpoint on a publicly reachable server without authorize or bearerToken.

HSX Lens

The @srdjan/hsx-lens package is an opt-in development workbench for the hypermedia contract your app already declares. It renders explicit page samples, collects author-time HSX attributes before they become hx-*, mirrors component/widget/agent metadata, and serves a local manifest plus an HTML workbench. It does not call component handlers.

import { createHsxLens } from "@srdjan/hsx-lens";

const lens = createHsxLens({
  appName: "Todos",
  pages: [{ name: "Home", path: "/", render: () => <Page.Component /> }],
  components: todoComponents,
});

Deno.serve((req) => {
  const lensResponse = lens.handle(req);
  if (lensResponse) return lensResponse;

  // Your app routes...
  return new Response("Not found", { status: 404 });
});

By default, the workbench is served at /__hsx and the JSON manifest at /__hsx/manifest.json. Mount it only in local or trusted development environments.

API Reference

render(node, options?)

Renders JSX to an HTTP Response.

render(<Page />, {
  status: 200, // HTTP status code
  headers: {}, // Additional response headers
  maxDepth: 100, // Max nesting depth (DoS protection)
  maxNodes: 50000, // Max node count (DoS protection)
  injectHtmx: undefined, // true/false to force, undefined for auto
});

renderHtml(node, options?)

Renders JSX to an HTML string.

const html = renderHtml(<Page />, {
  maxDepth: 100,
  maxNodes: 50000,
  injectHtmx: undefined,
});

route(path)

Creates a type-safe route template. bind() requires exactly the parameters extracted from :param segments and returns the final URL value used in JSX.

const r = route("/users/:id");
// r.path = "/users/:id"
// r.bind({ id: 42 }).path = "/users/42"
// r.match("/users/42") = { id: "42" }

id(name)

Creates a branded element ID with # prefix.

const listId = id("todo-list");
// Type: Id<"todo-list">
// Value: "#todo-list"

Fragment

JSX Fragment for grouping elements without a wrapper.

<Fragment>
  <li>One</li>
  <li>Two</li>
</Fragment>;

hsxComponent(path, options)

Co-locates a route, request handler, and render function.

const Comp = hsxComponent("/items/:id", {
  methods: ["GET", "DELETE"], // defaults to ["GET"]
  status: 200,
  headers: { "x-powered-by": "hsx" },
  handler: async (_req, params) => ({
    item: await getItem(params.id),
  }),
  render: ({ item }) => <div>{item.name}</div>,
});

// Bind the component route before using it in JSX:
<button delete={Comp.bind({ id: 42 })} target="#row-42" />;

Unexpected handler or render errors return a generic 500 response, while handlers can throw HsxHttpError for intentional boundary failures such as invalid input.

Examples

Run examples with deno task:

Example Command Description
Todos deno task example:todos Full CRUD with partial updates
Active Search deno task example:active-search Live search as you type
Lazy Loading deno task example:lazy-loading Deferred content loading
Form Validation deno task example:form-validation Server-side validation
Polling deno task example:polling Live dashboard with intervals
Auras Showcase deno task example:auras-showcase First-impression demo, live scene playground, compact reference
Tabs & Modal deno task example:tabs-modal Tab navigation and modals
HSX Components deno task example:hsx-components Co-located route + handler + render
HSX Page deno task example:hsx-page Semantic full-page with hsxPage guardrails
Low-Level API deno task example:low-level-api Manual render/renderHtml without hsxPage/hsxComponent
HSX Widget deno task example:hsx-widget Widget SSR route + iframe embed shell
Todos Copilot deno task example:todos-copilot AI copilot driving real hsxComponent endpoints
Index of examples examples/README.md Quick guide to pick the right example

For the HSX widget example, build client assets first:

deno task build:hsx-widgets
deno task example:hsx-widget

Safety

  • HTML escaping - All text content and attributes are escaped (XSS prevention)
  • Raw text elements - <script> and <style> children are NOT escaped. Never pass user input.
  • No manual hx-* - Throws at render time. Use HSX aliases instead.
  • DoS protection - Optional maxDepth and maxNodes limits

Project Structure

packages/
  hsx/                   # Core package (@srdjan/hsx)
    mod.ts               # Main entry point
    jsx-runtime.ts       # Minimal JSX runtime (compiler requirement)
    render.ts            # SSR renderer with HTMX injection
    sse.ts               # SSE response helper (renderSSE, encodeSSEFrame)
    loading.ts           # Loading placeholder component
    hsx-normalize.ts     # HSX to hx-* attribute mapping
    hsx-types.ts         # Route, Id, HsxSwap, HsxTrigger types
    hsx-component.ts     # hsxComponent factory (route + handler + render)
    hsx-page.ts          # hsxPage guardrail for full-page layouts
    htmx-config.ts       # HtmxConfig meta tag (noSwap and other HTMX config)
    bus.ts               # HsxBus config meta tag
    core.ts              # Tree-shaking entry: render, route, id, Fragment
    components.ts        # Tree-shaking entry: hsxComponent, hsxPage
    runtime/hsx-bus.js   # Optional browser runtime: emit/on/act and SSE
  hsx-styles/            # Styles package (@srdjan/hsx-styles)
    mod.ts               # Entry point (bundles CSS strings)
    hsx-brand.css        # HSX brand layer
    vendor/              # Vendored Auras foundation
  hsx-widgets/           # HSX widgets package (@srdjan/hsx-widgets)
    mod.ts               # Main entry point
    widget.ts            # Widget protocol
    widget-wrapper.ts    # Shared Light DOM / Shadow DOM wrapping
    ssr-adapter.ts       # Widget -> hsxComponent bridge
    styles.ts            # Style collection for hsxPage
    result.ts            # Result<T,E> type utilities
    genui-widget.ts      # GenUIWidget<P> type (Widget + AI metadata)
    catalog.ts           # Widget catalog and tool definition generation
    design-guidelines.ts # AI-readable design system constraints
    embed/               # Embed helpers (iframe shell + snippet)
    build/               # Dual-compile build pipeline (esbuild + Preact)
  hsx-genui/             # Generative UI package (@srdjan/hsx-genui)
    mod.ts               # Main entry point
    provider.ts          # AIProvider port (Message, ToolCall, StreamEvent)
    agent-loop.ts        # Shared provider/tool execution loop
    handler.ts           # GenUI handleTurn adapter
    conversation.ts      # Durable repository port + in-memory replay adapter
    components.tsx       # Chat page, send route, and replayable SSE route
    providers/
      claude.ts          # Claude/Anthropic adapter (raw fetch + SSE)
  hsx-agent/             # Agent-operable apps package (@srdjan/hsx-agent)
    mod.ts               # Main entry point
    app-agent.ts         # createAppAgent (AI drives real components)
    registry.ts          # Ajv 2020-12 tool registry and argument validation
    request-build.ts     # toRequest (validated args + source Request -> Request)
    types.ts             # AgentComponent and AgentToolInvoker boundaries
  hsx-mcp/               # MCP server package (@srdjan/hsx-mcp)
    mod.ts               # Main entry point
    mcp-handler.ts       # createMcpHandler (Streamable HTTP endpoint)
    protocol.ts          # MCP method dispatch (initialize/tools/resources)
    versions.ts          # Supported protocol versions and manifest defaults
    jsonrpc.ts           # Hand-rolled JSON-RPC 2.0 envelope
    types.ts             # McpHandlerOptions, McpHandler
  hsx-lens/              # Dev workbench package (@srdjan/hsx-lens)
    mod.ts               # Main entry point
    manifest.ts          # createHsxManifest (samples -> manifest)
    workbench.tsx        # createHsxLens (local HTML + JSON routes)
examples/
  todos/                 # Full todo app example
  todos-copilot/         # Todos with an AI copilot driving real endpoints
  active-search/         # Search example
  lazy-loading/          # Lazy load example
  form-validation/       # Form validation example
  polling/               # Polling example
  tabs-modal/            # Tabs and modal example
  hsx-components/        # HSX Component pattern example
  hsx-page/              # hsxPage full-page guardrail example
  low-level-api/         # Manual render/renderHtml without hsxPage/hsxComponent
  hsx-widget/            # HSX widget SSR + embed shell example
  auras-showcase/        # Auras design system with a live HSX scene playground
  ats/                   # Light DOM custom elements styled by host CSS
  event-bus/             # Client-side pub/sub: filters, toast, optimistic add
vendor/htmx/
  htmx.js                # Vendored HTMX 4.0.0
  htmx.d.ts              # HTMX config and API types
docs/
  EXAMPLES.md            # Full examples matrix
  USER_GUIDE.md          # Comprehensive user guide
  HSX_OVERVIEW.md        # Architecture overview
  HTMX_INTEGRATION.md    # HTMX integration details
  ROUTES_AND_IDS.md      # route() and id() reference
  WIDGETS.md             # HSX widget guide and embed workflow
  plans/                 # Dated plans and implementation records
  solutions/             # Documented fixes and decisions

License

MIT - see LICENSE

About

SSR-only JSX/TSX renderer for Deno that hides HTMX away :)

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages