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 warningsThe 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 agentsOne 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 MapandSet, read like objects and arrays- class instances, whose getters are fields:
{user.initial}readsget initial() - values with a
toJSONmethod, likeDate, 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