hypra

hypra for JavaScript

The Hypra compiler, and the runtime for the pages it compiles to JavaScript. A page renders at request time in Node, Bun or Deno, from Express, Hono, Fastify, Next.js route handlers or any other server: as HTML for browsers, Markdown for LLMs, JSON for apps and WebMCP tools for AI agents.

$ npm install hypra
$ npx hypra build views/product.hypra --target js --out views/dist
โœ” views/dist/product.mjs
โœ” views/dist/product.d.mts
โœ” views/dist/product.sources.json
0 errors, 0 warnings

The package brings the compiler for Linux, macOS and Windows on x64 and ARM; nothing is downloaded when it's installed. npx hypra check and npx hypra fmt work the same way.

Rendering a page

A compiled page is an ES module. Its render takes the page's data, the props it declares:

import product from "./views/dist/product.mjs";

const output = product.render({ product: await db.products.find(42) });

output.html;      // the page
output.markdown;  // for LLMs
output.json;      // page metadata and actions
output.webmcp;    // tool definitions for AI agents

One URL can serve all of them. negotiate picks what the request's Accept header asks for: Markdown for text/markdown, JSON for application/json, HTML for everything else.

import express from "express";
import product from "./views/dist/product.mjs";

const app = express();

app.get("/shoes/:id", async (req, res) => {
  const output = product.render({ product: await db.products.find(req.params.id) });
  const { contentType, body } = output.negotiate(req.get("accept"));
  res.vary("Accept").type(contentType).send(body);
});

With Hono, or anything else built on the web's Request and Response:

app.get("/shoes/:id", async (c) => {
  const output = product.render({ product: await db.products.find(c.req.param("id")) });
  const { contentType, body } = output.negotiate(c.req.header("accept"));
  return c.body(body, 200, { "Content-Type": contentType, Vary: "Accept" });
});

Options

render(data, options) takes:

Option Meaning
strict throw a DataError naming the .hypra line when data is missing, instead of rendering it empty
formFields a function returning hidden fields for POST forms to this site, e.g. () => ({ _csrf: req.csrfToken() })
alternateUrl where the page's Markdown is served; adds <link rel="alternate" type="text/markdown">
import { DataError } from "hypra/runtime";

try {
  product.render({}, { strict: true });
} catch (error) {
  if (error instanceof DataError) console.error(error.message);
  // views/product.hypra:13:9: prop `product` is missing from the data
}

Hidden form fields only go to forms that post to this site, never to a form whose address leads somewhere else.

Data

Data is what JSON can hold, plus what JavaScript apps pass around:

  • objects, arrays, strings, numbers, booleans and null
  • Map and Set, read like objects and arrays
  • class instances, whose getters are fields: {user.initial} reads get initial()
  • values with a toJSON method, like Date, which show as what they turn into: 2026-01-02T00:00:00.000Z

A page renders exactly as hypra build writes it with the same data as JSON; the tests compare the two on thousands of generated pages. One difference comes from JavaScript itself: it has one kind of number, so 1.0 from JSON shows as 1, where the compiler's static build writes 1.0.

TypeScript

The package has types, and the compiler writes a .d.mts next to every page, so render, negotiate and the output are typed:

import product from "./views/dist/product.mjs";
import type { Output } from "hypra/runtime";

const output: Output = product.render({ product });

Rebuilding

product.sources.json lists the .hypra files a page was built from, so a build script or a file watcher knows which pages to rebuild when one of them changes.

License

MIT OR Apache-2.0