@constela/server
v19.0.0
Published
Server-side rendering for Constela UI framework
Readme
@constela/server
Server-side rendering (SSR) for Constela JSON programs.
Installation
npm install @constela/serverPeer Dependencies:
@constela/compiler^0.7.0
How It Works
JSON program → HTML string
{
"version": "1.0",
"state": { "name": { "type": "string", "initial": "World" } },
"view": {
"kind": "element",
"tag": "h1",
"children": [
{ "kind": "text", "value": { "expr": "lit", "value": "Hello, " } },
{ "kind": "text", "value": { "expr": "state", "name": "name" } }
]
}
}↓ SSR
<h1>Hello, World</h1>Features
String Concatenation (concat)
Build dynamic strings during SSR:
{
"kind": "element",
"tag": "a",
"props": {
"href": {
"expr": "concat",
"items": [
{ "expr": "lit", "value": "/posts/" },
{ "expr": "data", "name": "post", "path": "slug" }
]
}
}
}↓ SSR
<a href="/posts/hello-world">...</a>Markdown Rendering
{
"kind": "markdown",
"content": { "expr": "data", "name": "article", "path": "content" }
}Rendered with async parsing and Shiki syntax highlighting.
Code Highlighting
{
"kind": "code",
"code": { "expr": "lit", "value": "const x = 1;" },
"language": { "expr": "lit", "value": "typescript" }
}Features:
- Dual theme support (github-light, github-dark)
- CSS custom properties for theme switching
- Preloaded languages: javascript, typescript, json, html, css, python, rust, go, java, bash, markdown
Call/Lambda Expressions
Full support for call and lambda expressions during SSR:
{
"expr": "call",
"target": { "expr": "data", "name": "posts" },
"method": "filter",
"args": [{
"expr": "lambda",
"param": "post",
"body": { "expr": "get", "base": { "expr": "var", "name": "post" }, "path": "published" }
}]
}Route Context
Pass route parameters for dynamic pages:
{
"route": { "path": "/users/:id" },
"view": {
"kind": "text",
"value": { "expr": "route", "name": "id", "source": "param" }
}
}Import Data
Pass external data at render time:
{
"imports": { "config": "./data/config.json" },
"view": {
"kind": "text",
"value": { "expr": "import", "name": "config", "path": "siteName" }
}
}Style Evaluation
Style expressions are evaluated during SSR, producing CSS class strings:
{
"styles": {
"button": {
"base": "px-4 py-2 rounded",
"variants": {
"variant": {
"primary": "bg-blue-500 text-white",
"secondary": "bg-gray-200 text-gray-800"
}
},
"defaultVariants": { "variant": "primary" }
}
},
"view": {
"kind": "element",
"tag": "button",
"props": {
"className": {
"expr": "style",
"name": "button",
"variants": { "variant": { "expr": "lit", "value": "primary" } }
}
}
}
}↓ SSR
<button class="px-4 py-2 rounded bg-blue-500 text-white">...</button>Pass style presets via RenderOptions.styles for evaluation.
Output Structure
Code Block HTML
<div class="constela-code" data-code-content="...">
<div class="group relative">
<div class="language-badge">typescript</div>
<button class="constela-copy-btn"><!-- Copy icon --></button>
<pre><code class="shiki">...</code></pre>
</div>
</div>CSS Variables
/* Light mode */
.shiki { background-color: var(--shiki-light-bg); }
.shiki span { color: var(--shiki-light); }
/* Dark mode */
.dark .shiki { background-color: var(--shiki-dark-bg); }
.dark .shiki span { color: var(--shiki-dark); }Streaming SSR
Render to a ReadableStream for progressive HTML delivery:
import { renderToStream, createHtmlTransformStream } from '@constela/server';
// Render program to stream
const contentStream = renderToStream(compiledProgram, {
flushStrategy: 'batched',
}, {
route: { params: { id: '123' }, query: {}, path: '/posts/123' },
imports: { config: siteConfig },
});
// Wrap with HTML document structure
const htmlStream = contentStream.pipeThrough(
createHtmlTransformStream({
title: 'My Page',
lang: 'en',
stylesheets: ['/styles.css'],
scripts: ['/client.js'],
})
);
// Use with Response (Edge/Workers)
return new Response(htmlStream, {
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});Flush Strategies:
| Strategy | Description |
|----------|-------------|
| immediate | Flush each chunk as soon as it's ready |
| batched | Flush when buffer exceeds 1KB threshold |
| manual | Only flush at the end (for small pages) |
StreamingRenderOptions:
interface StreamingRenderOptions {
flushStrategy: 'immediate' | 'batched' | 'manual';
}Abort Signal Support
Cancel streaming when the client disconnects:
const controller = new AbortController();
const stream = renderToStream(program, { flushStrategy: 'batched' }, {
signal: controller.signal,
});
// Cancel on client disconnect
request.signal.addEventListener('abort', () => {
controller.abort();
});Suspense Boundaries
Server-side suspense for async content:
{
"view": {
"kind": "suspense",
"id": "user-data",
"fallback": {
"kind": "element",
"tag": "div",
"props": { "className": { "expr": "lit", "value": "skeleton" } },
"children": []
},
"content": {
"kind": "component",
"name": "UserProfile",
"props": { "user": { "expr": "data", "name": "user" } }
}
}
}Renders with markers for client-side hydration:
<div data-suspense-id="user-data">
<!-- Fallback content first -->
<div class="skeleton"></div>
</div>
<!-- Resolved content follows -->Security
- HTML Escaping - All text output is escaped
- DOMPurify - Markdown content is sanitized
- Prototype Pollution Prevention - Same as runtime
Internal API
For framework developers only. End users should use the CLI.
renderToString
import { renderToString } from '@constela/server';
const html = await renderToString(compiledProgram, {
route: {
params: { id: '123' },
query: { tab: 'overview' },
path: '/users/123',
},
imports: {
config: { siteName: 'My Site' },
},
styles: {
button: {
base: 'px-4 py-2 rounded',
variants: {
variant: {
primary: 'bg-blue-500 text-white',
secondary: 'bg-gray-200 text-gray-800',
},
},
defaultVariants: { variant: 'primary' },
},
},
});RenderOptions:
interface RenderOptions {
route?: {
params?: Record<string, string>;
query?: Record<string, string>;
path?: string;
};
imports?: Record<string, unknown>;
styles?: Record<string, StylePreset>;
}
interface StylePreset {
base: string;
variants?: Record<string, Record<string, string>>;
defaultVariants?: Record<string, string>;
}Integration with @constela/runtime
Server-rendered HTML can be hydrated on the client:
{
"version": "1.0",
"lifecycle": { "onMount": "initializeClient" },
"state": { ... },
"actions": [
{
"name": "initializeClient",
"steps": [
{ "do": "storage", "operation": "get", "key": { "expr": "lit", "value": "preferences" }, ... }
]
}
],
"view": { ... }
}License
MIT
