Constela
Constela is a compiler-first UI language designed for vibecoding.
Unlike React or Next.js, you do not write UI with JavaScript. You describe UI behavior as a constrained JSON DSL, which is validated, analyzed, and compiled into minimal runtime code.
Constela is optimized for:
- AI-generated UI
- deterministic behavior
- inspectable and debuggable state transitions
Mental Model
| Constela | React / Next.js | |
|---|---|---|
| UI authoring | JSON DSL | JavaScript / JSX |
| Execution | compiler-driven | runtime-driven |
| State updates | declarative actions | arbitrary JS |
| Errors | structured errors | runtime exceptions |
Measured Differences
We rebuilt Constela's official website using both Constela and Next.js and compared the results. The measurements were repeated multiple times and showed consistent trends.
| Metric | Constela | Next.js | Difference |
|---|---|---|---|
| Build time | 2.2s | 12.3s | 5.6× faster |
| node_modules size | 297MB | 794MB | 2.7× smaller |
| Output size | 14MB | 72MB | 5.1× smaller |
| Deploy time | 10s | 50s | 5.0× faster |
This is not an accidental optimization, but a structural difference:
- Next.js is a full application framework with routing analysis, bundling, optimization, and runtime setup.
- Constela is a compiler-first UI language. JSON is validated and compiled directly into minimal output.
Note: Performance is not Constela's primary goal. The core value lies in compile-time validation and safe UI generation. These characteristics are a direct consequence of its design.
Quick Start
Create a New Project
The fastest way to get started with Constela:
npx create-constela my-app
cd my-app
npm run devOpen http://localhost:3000 to see your app.
Manual Installation
# Recommended: Full-stack development npm install @constela/start # Low-level API (advanced) npm install @constela/runtime @constela/compiler
Basic Usage
- Create a project:
mkdir my-app && cd my-app npm init -y npm install @constela/start mkdir -p src/routes
- Create a page (
src/routes/index.constela.json):
{
"version": "1.0",
"state": {
"count": { "type": "number", "initial": 0 }
},
"actions": [
{
"name": "increment",
"steps": [{ "do": "update", "target": "count", "operation": "increment" }]
}
],
"view": {
"kind": "element",
"tag": "button",
"props": { "onClick": { "event": "click", "action": "increment" } },
"children": [{ "kind": "text", "value": { "expr": "state", "name": "count" } }]
}
}- Start the dev server:
Open http://localhost:3000 to see your app.
DSL Overview
Constela programs are JSON documents with this structure:
{
"version": "1.0",
"route": { ... },
"imports": { ... },
"data": { ... },
"lifecycle": { ... },
"state": { ... },
"actions": [ ... ],
"view": { ... },
"components": { ... }
}All fields except version, state, actions, and view are optional.
State
Declare application state with explicit types:
{
"state": {
"count": { "type": "number", "initial": 0 },
"query": { "type": "string", "initial": "" },
"items": { "type": "list", "initial": [] },
"isVisible": { "type": "boolean", "initial": true },
"form": { "type": "object", "initial": { "name": "", "email": "" } }
}
}State types: number, string, list, boolean, object
View Nodes
12 node types for building UI:
// Element node { "kind": "element", "tag": "div", "props": { "className": "container" }, "children": [...] } // Text node { "kind": "text", "value": { "expr": "state", "name": "count" } } // Conditional node { "kind": "if", "condition": { "expr": "state", "name": "visible" }, "then": {...}, "else": {...} } // Loop node { "kind": "each", "items": { "expr": "state", "name": "todos" }, "as": "item", "body": {...} } // Portal node (render children to a different DOM location) { "kind": "portal", "target": "body", "children": [...] } // Component node { "kind": "component", "name": "Button", "props": { "label": { "expr": "lit", "value": "Click" } } } // Slot node (insertion point for children in components/layouts) { "kind": "slot" } // Markdown node { "kind": "markdown", "content": { "expr": "state", "name": "markdownContent" } } // Code node (syntax-highlighted code block) { "kind": "code", "code": { "expr": "lit", "value": "const x = 42;" }, "language": { "expr": "lit", "value": "typescript" } } // Island node (client-side interactive region within SSR) { "kind": "island", "component": "Counter", "props": {...} } // Suspense node (async loading boundary) { "kind": "suspense", "fallback": {...}, "children": [...] } // Error boundary node { "kind": "errorBoundary", "fallback": {...}, "children": [...] }
Portal targets: body, head, or any CSS selector
Expressions
Constrained expression system (no arbitrary JavaScript):
// Literal { "expr": "lit", "value": "Hello" } // State reference { "expr": "state", "name": "count" } // Loop variable reference { "expr": "var", "name": "item" } // Binary operation { "expr": "bin", "op": "+", "left": {...}, "right": {...} } // Negation { "expr": "not", "operand": {...} } // Conditional (if/then/else) { "expr": "cond", "if": {...}, "then": {...}, "else": {...} } // Property access { "expr": "get", "base": { "expr": "state", "name": "user" }, "path": "address.city" } // Route parameter (requires route definition) { "expr": "route", "name": "id", "source": "param" } // Imported data reference (requires imports field) { "expr": "import", "name": "navigation", "path": "items" } // Build-time data reference (requires data field) { "expr": "data", "name": "posts", "path": "0.title" } // Form validation state (requires ref on form element) { "expr": "validity", "ref": "emailInput", "property": "valid" } { "expr": "validity", "ref": "emailInput", "property": "message" }
Binary operators: +, -, *, /, %, ==, !=, <, <=, >, >=, &&, ||
Route sources: param (default), query, path
Validity properties: valid, valueMissing, typeMismatch, patternMismatch, tooLong, tooShort, rangeUnderflow, rangeOverflow, customError, message
Actions
Named actions with declarative steps:
{
"actions": [
{
"name": "increment",
"steps": [
{ "do": "update", "target": "count", "operation": "increment" }
]
},
{
"name": "addTodo",
"steps": [
{ "do": "update", "target": "todos", "operation": "push", "value": {...} },
{ "do": "set", "target": "input", "value": { "expr": "lit", "value": "" } }
]
}
]
}Step types:
set- Set state valueupdate- Update with operation (see below)fetch- HTTP request withonSuccess/onErrorhandlersstorage- localStorage/sessionStorage operationsclipboard- Clipboard read/writenavigate- Page navigationdelay- Execute steps after a delayinterval- Execute action periodicallyclearTimer- Stop a running timerfocus- Focus, blur, or select form elements
Update operations:
| Operation | State Type | Required Fields | Description |
|---|---|---|---|
increment |
number | - | Add to number (default: 1) |
decrement |
number | - | Subtract from number (default: 1) |
push |
list | value |
Add item to end of array |
pop |
list | - | Remove last item from array |
remove |
list | value |
Remove item by value or index |
toggle |
boolean | - | Flip boolean value |
merge |
object | value |
Shallow merge into object |
replaceAt |
list | index, value |
Replace item at index |
insertAt |
list | index, value |
Insert item at index |
splice |
list | index, deleteCount |
Delete/insert items |
// Toggle boolean { "do": "update", "target": "isOpen", "operation": "toggle" } // Merge object { "do": "update", "target": "form", "operation": "merge", "value": { "expr": "lit", "value": { "name": "John" } } } // Replace at index { "do": "update", "target": "items", "operation": "replaceAt", "index": { "expr": "lit", "value": 0 }, "value": {...} } // Insert at index { "do": "update", "target": "items", "operation": "insertAt", "index": { "expr": "lit", "value": 1 }, "value": {...} } // Splice (delete 2 items at index 1, insert new items) { "do": "update", "target": "items", "operation": "splice", "index": { "expr": "lit", "value": 1 }, "deleteCount": { "expr": "lit", "value": 2 }, "value": { "expr": "lit", "value": ["a", "b"] } }
Browser Actions
// Storage (localStorage/sessionStorage) { "do": "storage", "operation": "get", "key": { "expr": "lit", "value": "theme" }, "storage": "local", "result": "savedTheme", "onSuccess": [ { "do": "set", "target": "theme", "value": { "expr": "var", "name": "savedTheme" } } ] } { "do": "storage", "operation": "set", "key": { "expr": "lit", "value": "theme" }, "value": { "expr": "state", "name": "theme" }, "storage": "local" } { "do": "storage", "operation": "remove", "key": { "expr": "lit", "value": "theme" }, "storage": "local" } // Clipboard { "do": "clipboard", "operation": "write", "value": { "expr": "state", "name": "textToCopy" } } { "do": "clipboard", "operation": "read", "result": "clipboardText" } // Navigate { "do": "navigate", "url": { "expr": "lit", "value": "/about" } } { "do": "navigate", "url": { "expr": "lit", "value": "https://example.com" }, "target": "_blank" } { "do": "navigate", "url": { "expr": "state", "name": "redirectUrl" }, "replace": true }
Storage operations: get, set, remove
Storage types: local, session
Clipboard operations: write, read
Navigate targets: _self (default), _blank
Timer Actions
// Delay execution { "do": "delay", "ms": { "expr": "lit", "value": 2000 }, "then": [ { "do": "set", "target": "message", "value": { "expr": "lit", "value": "Delayed!" } } ] } // Periodic execution (returns timer ID) { "do": "interval", "ms": { "expr": "lit", "value": 5000 }, "action": "fetchData", "result": "pollTimerId" } // Stop a timer { "do": "clearTimer", "target": { "expr": "state", "name": "pollTimerId" } }
Timer operations:
delay- Executethensteps aftermsmillisecondsinterval- Executeactioneverymsmilliseconds, stores timer ID inresultclearTimer- Stop a running timer by its ID
Form Actions
// Focus an input element { "do": "focus", "target": { "expr": "ref", "name": "emailInput" }, "operation": "focus" } // Select text in an input { "do": "focus", "target": { "expr": "ref", "name": "codeInput" }, "operation": "select" } // Blur (unfocus) an element { "do": "focus", "target": { "expr": "ref", "name": "searchInput" }, "operation": "blur" }
Focus operations: focus, blur, select
Advanced Actions
DOM Manipulation
Manipulate DOM elements via refs:
// Define a ref on an element { "kind": "element", "tag": "div", "ref": "myElement", "children": [...] } // Manipulate via action { "do": "dom", "operation": "addClass", "ref": "myElement", "value": { "expr": "lit", "value": "active" } } { "do": "dom", "operation": "removeClass", "ref": "myElement", "value": { "expr": "lit", "value": "active" } } { "do": "dom", "operation": "toggleClass", "ref": "myElement", "value": { "expr": "lit", "value": "visible" } } { "do": "dom", "operation": "setAttribute", "ref": "myElement", "attr": "data-state", "value": { "expr": "state", "name": "currentState" } } { "do": "dom", "operation": "removeAttribute", "ref": "myElement", "attr": "disabled" }
DOM operations: addClass, removeClass, toggleClass, setAttribute, removeAttribute
Dynamic Imports & External Libraries
Import external JavaScript modules and call their methods:
// Import a module { "do": "import", "module": "chart.js", "result": "Chart" } // Call a method on the imported module { "do": "call", "ref": "Chart", "method": "create", "args": [{ "expr": "ref", "name": "canvas" }, { "expr": "state", "name": "chartConfig" }], "result": "chartInstance" } // Subscribe to events { "do": "subscribe", "ref": "eventSource", "event": "message", "action": "handleMessage" } // Dispose resources { "do": "dispose", "ref": "chartInstance" }
Note: Subscriptions are automatically disposed on component unmount.
Markdown & Code Blocks
Render Markdown content and syntax-highlighted code:
// Markdown node { "kind": "markdown", "content": { "expr": "state", "name": "markdownContent" } } // Code block with syntax highlighting { "kind": "code", "code": { "expr": "lit", "value": "const x: number = 42;" }, "language": { "expr": "lit", "value": "typescript" } }
Features:
- Markdown rendered with marked
- Code highlighting with Shiki
- Dual theme support (light/dark)
- Built-in copy button
Components
Reusable view definitions with props and slots:
{
"components": {
"Button": {
"params": {
"label": { "type": "string" },
"disabled": { "type": "boolean", "required": false }
},
"view": {
"kind": "element",
"tag": "button",
"props": { "disabled": { "expr": "param", "name": "disabled" } },
"children": [
{ "kind": "text", "value": { "expr": "param", "name": "label" } }
]
}
},
"Card": {
"params": { "title": { "type": "string" } },
"view": {
"kind": "element",
"tag": "div",
"children": [
{ "kind": "text", "value": { "expr": "param", "name": "title" } },
{ "kind": "slot" }
]
}
}
}
}Using components:
{
"kind": "component",
"name": "Card",
"props": { "title": { "expr": "lit", "value": "My Card" } },
"children": [
{ "kind": "text", "value": { "expr": "lit", "value": "Card content goes here" } }
]
}Param types: string, number, boolean, json
Param expression:
{ "expr": "param", "name": "label" }
{ "expr": "param", "name": "user", "path": "name" }Component Local State
Components can have their own independent local state that is not shared with other instances:
{
"components": {
"Accordion": {
"params": { "title": { "type": "string" } },
"localState": {
"isExpanded": { "type": "boolean", "initial": false }
},
"localActions": [
{
"name": "toggle",
"steps": [{ "do": "update", "target": "isExpanded", "operation": "toggle" }]
}
],
"view": {
"kind": "element",
"tag": "div",
"children": [
{
"kind": "element",
"tag": "button",
"props": {
"onClick": { "event": "click", "action": "toggle" }
},
"children": [
{ "kind": "text", "value": { "expr": "param", "name": "title" } }
]
},
{
"kind": "if",
"condition": { "expr": "state", "name": "isExpanded" },
"then": { "kind": "slot" }
}
]
}
}
}
}Key features:
- Each component instance maintains its own state
localStateuses the same syntax as globalstatelocalActionscan only useset,update, andsetPathsteps (nofetch,navigate, etc.)- Access local state with
{ "expr": "state", "name": "isExpanded" }within the component
Use cases:
- Accordion / Collapsible sections
- Dropdowns and tooltips
- Form field validation state
- Toggle switches
- Any component that needs internal state without polluting global state
Event Handling
Bind events to actions via props:
{
"kind": "element",
"tag": "button",
"props": {
"onClick": { "event": "click", "action": "increment" }
}
}For input events with payload:
{
"props": {
"onInput": { "event": "input", "action": "setQuery", "payload": { "expr": "var", "name": "value" } }
}
}Debounce & Throttle:
// Debounce: Wait 300ms after last event before executing { "event": "input", "action": "search", "debounce": 300 } // Throttle: Execute at most once per 100ms { "event": "scroll", "action": "trackScroll", "throttle": 100 }
IntersectionObserver (visibility tracking):
{
"onIntersect": {
"event": "intersect",
"action": "loadMore",
"options": { "threshold": 0.5, "rootMargin": "100px" }
}
}Available event data variables:
| Event Type | Available Variables |
|---|---|
| Input | value, checked |
| Keyboard | key, code, ctrlKey, shiftKey, altKey, metaKey |
| Mouse | clientX, clientY, pageX, pageY, button |
| Touch | touches (array with clientX, clientY, pageX, pageY) |
| Scroll | scrollTop, scrollLeft |
| File Input | files (array with name, size, type) |
Route Definition
Define page routes with path, layout, and metadata:
{
"route": {
"path": "/users/:id",
"title": { "expr": "bin", "op": "+", "left": { "expr": "lit", "value": "User: " }, "right": { "expr": "route", "name": "id" } },
"layout": "MainLayout",
"meta": {
"description": { "expr": "lit", "value": "User profile page" }
}
}
}Access route params in expressions with { "expr": "route", "name": "id" }.
Meta Tag Generation
route.title and route.meta automatically generate HTML meta tags at build time.
Example route definition:
{
"route": {
"path": "/posts/:slug",
"title": {
"expr": "concat",
"items": [
{ "expr": "route", "name": "slug", "source": "param" },
{ "expr": "lit", "value": " | My Blog" }
]
},
"meta": {
"description": { "expr": "lit", "value": "Read our latest blog posts" },
"og:title": { "expr": "route", "name": "slug", "source": "param" },
"og:type": { "expr": "lit", "value": "article" },
"og:url": {
"expr": "concat",
"items": [
{ "expr": "lit", "value": "https://example.com" },
{ "expr": "route", "name": "", "source": "path" }
]
},
"twitter:card": { "expr": "lit", "value": "summary_large_image" }
}
}
}Generated HTML (for /posts/hello-world):
<title>hello-world | My Blog</title> <meta name="description" content="Read our latest blog posts"> <meta property="og:title" content="hello-world"> <meta property="og:type" content="article"> <meta property="og:url" content="https://example.com/posts/hello-world"> <meta property="twitter:card" content="summary_large_image">
Tag generation rules:
| Key | Generated Tag |
|---|---|
title |
<title>...</title> |
og:* |
<meta property="og:*" content="..."> |
twitter:* |
<meta property="twitter:*" content="..."> |
| Other | <meta name="*" content="..."> |
Supported expressions for dynamic values:
{ "expr": "lit", "value": "static text" }- Literal value{ "expr": "route", "name": "slug", "source": "param" }- Route parameter{ "expr": "route", "name": "q", "source": "query" }- Query parameter{ "expr": "route", "name": "", "source": "path" }- Current path{ "expr": "concat", "items": [...] }- Concatenate multiple expressions
Canonical URL
Set canonical URL via route.canonical:
{
"route": {
"path": "/posts/:slug",
"canonical": {
"expr": "bin",
"op": "+",
"left": { "expr": "lit", "value": "https://example.com" },
"right": { "expr": "route", "source": "path" }
}
}
}Output (for /posts/hello-world):
<link rel="canonical" href="https://example.com/posts/hello-world">
JSON-LD Structured Data
Add structured data via route.jsonLd:
{
"route": {
"path": "/posts/:slug",
"jsonLd": {
"type": "Article",
"properties": {
"headline": { "expr": "route", "name": "slug", "source": "param" },
"author": {
"expr": "object",
"type": "Person",
"properties": {
"name": { "expr": "lit", "value": "John Doe" }
}
},
"datePublished": { "expr": "lit", "value": "2024-01-15" }
}
}
}
}Output:
<script type="application/ld+json"> {"@context":"https://schema.org","@type":"Article","headline":"hello-world","author":{"@type":"Person","name":"John Doe"},"datePublished":"2024-01-15"} </script>
JSON-LD features:
- Nested objects with
{ "expr": "object", "type": "Person", "properties": {...} } - Arrays with
{ "expr": "array", "items": [...] } - Dynamic values using any expression type
- XSS protection (automatically escapes
</script>and other dangerous sequences)
Imports
Import external JSON data files:
{
"imports": {
"navigation": "./data/navigation.json",
"config": "./data/site-config.json"
}
}Access imported data with { "expr": "import", "name": "navigation", "path": "items" }.
Data Sources
Load data at build time for static site generation:
{
"data": {
"posts": {
"type": "glob",
"pattern": "content/blog/*.mdx",
"transform": "mdx"
},
"config": {
"type": "file",
"path": "data/config.json"
},
"users": {
"type": "api",
"url": "https://api.example.com/users"
}
},
"route": {
"path": "/posts/:slug",
"getStaticPaths": {
"source": "posts",
"params": {
"slug": { "expr": "get", "base": { "expr": "var", "name": "item" }, "path": "slug" }
}
}
}
}Data source types: glob, file, api
Transforms: mdx, yaml, csv
Lifecycle Hooks
Execute actions on component lifecycle events:
{
"lifecycle": {
"onMount": "loadTheme",
"onUnmount": "saveState",
"onRouteEnter": "fetchData",
"onRouteLeave": "cleanup"
},
"actions": [
{
"name": "loadTheme",
"steps": [
{
"do": "storage",
"operation": "get",
"key": { "expr": "lit", "value": "theme" },
"storage": "local",
"result": "savedTheme",
"onSuccess": [
{ "do": "set", "target": "theme", "value": { "expr": "var", "name": "savedTheme" } }
]
}
]
}
]
}Layouts
Define reusable page layouts with slots:
{
"version": "1.0",
"type": "layout",
"view": {
"kind": "element",
"tag": "div",
"children": [
{ "kind": "component", "name": "Header" },
{ "kind": "element", "tag": "main", "children": [{ "kind": "slot" }] },
{ "kind": "component", "name": "Footer" }
]
}
}Pages reference layouts via route.layout. The page's view is inserted at the slot node.
Named slots are supported for multi-slot layouts:
{ "kind": "slot", "name": "sidebar" }Example: Counter
{
"version": "1.0",
"state": {
"count": { "type": "number", "initial": 0 }
},
"actions": [
{
"name": "increment",
"steps": [{ "do": "update", "target": "count", "operation": "increment" }]
},
{
"name": "decrement",
"steps": [{ "do": "update", "target": "count", "operation": "decrement" }]
}
],
"view": {
"kind": "element",
"tag": "div",
"children": [
{
"kind": "element",
"tag": "p",
"children": [
{ "kind": "text", "value": { "expr": "lit", "value": "Count: " } },
{ "kind": "text", "value": { "expr": "state", "name": "count" } }
]
},
{
"kind": "element",
"tag": "button",
"props": { "onClick": { "event": "click", "action": "decrement" } },
"children": [{ "kind": "text", "value": { "expr": "lit", "value": "-" } }]
},
{
"kind": "element",
"tag": "button",
"props": { "onClick": { "event": "click", "action": "increment" } },
"children": [{ "kind": "text", "value": { "expr": "lit", "value": "+" } }]
}
]
}
}Routing (via @constela/router)
Client-side routing is provided as a separate package that works alongside the core DSL. Note: Routing is NOT part of the DSL - it's an application-level add-on.
import { compile } from '@constela/compiler'; import { createRouter, bindLink } from '@constela/router'; // Compile multiple pages const homeProgram = compile(homeAst).program; const aboutProgram = compile(aboutAst).program; const userProgram = compile(userAst).program; // Create router const router = createRouter({ routes: [ { path: '/', program: homeProgram, title: 'Home' }, { path: '/about', program: aboutProgram, title: 'About' }, { path: '/users/:id', program: userProgram, title: ctx => `User ${ctx.params.id}` }, ], fallback: notFoundProgram, onRouteChange: (ctx) => { console.log('Route changed:', ctx.path, ctx.params); }, }); // Mount router const { destroy } = router.mount(document.getElementById('app')); // Programmatic navigation router.navigate('/about'); router.navigate('/users/123', { replace: true }); // Bind links for client-side navigation document.querySelectorAll('a[href]').forEach(a => bindLink(router, a));
Route Context:
{ path: string, // Current path params: Record<string, string>, // URL params (e.g., { id: '123' }) query: URLSearchParams // Query string params }
Note: Route params are now accessible in DSL expressions via { "expr": "route", "name": "id" } when using the route field in your program.
Dynamic Routes (via @constela/start)
For SSG with dynamic routes, export a function that receives route params:
// pages/docs/[...slug].ts import type { PageExportFunction, StaticPathsResult } from '@constela/start'; export const getStaticPaths = async (): Promise<StaticPathsResult> => ({ paths: [ { params: { slug: 'getting-started' } }, { params: { slug: 'api/components' } }, ] }); const page: PageExportFunction = async (params) => { const content = await loadMarkdown(`docs/${params.slug}.md`); return compileToProgram(content); }; export default page;
Static CompiledProgram exports continue to work for non-dynamic routes:
// pages/about.ts export default { version: '1.0', state: {}, actions: {}, view: { kind: 'element', tag: 'div', ... } };
Full-Stack Development (via @constela/start)
@constela/start provides a complete framework for building Constela applications:
Configuration
Configure your application via constela.config.json:
HTML lang Attribute
Set the lang attribute on <html>:
{
"seo": {
"lang": "ja"
}
}Output: <html lang="ja">
Supports all BCP 47 language tags including extended forms like zh-Hans-CN, de-DE-u-co-phonebk.
Dev Server
# Start development server
npx constela dev --port 3000Features:
- Vite-powered hot reload
- File-based routing
- Layout composition
- SSR with hydration
Hot Module Replacement (HMR)
The dev server includes built-in HMR that works automatically:
- Edit JSON, save, see changes - No manual refresh needed
- State is preserved - Form inputs, counters, and UI state survive updates
- Error overlay - Compile errors are shown with suggestions
- Auto-reconnect - Connection loss is handled gracefully
Just run npx constela dev and start editing your JSON files.
Production Build
# Build for production
npx constela build --outDir distGenerates:
- Static HTML for all routes
- Bundled runtime JavaScript
- Optimized assets
MDX Support
Transform Markdown with JSX components:
{
"data": {
"docs": {
"type": "glob",
"pattern": "content/docs/*.mdx",
"transform": "mdx"
}
}
}MDX files support:
- Frontmatter (YAML)
- Custom components
- Code blocks with syntax highlighting
- GitHub Flavored Markdown
API Routes
Create server-side API endpoints:
// pages/api/users.ts export const GET = async (ctx) => { return new Response(JSON.stringify({ users: [] }), { headers: { 'Content-Type': 'application/json' }, }); }; export const POST = async (ctx) => { const body = await ctx.request.json(); // Handle POST request };
Middleware
Add request middleware:
// pages/_middleware.ts export default async (ctx, next) => { console.log('Request:', ctx.url); const response = await next(); return response; };
Edge Deployment
Deploy to edge platforms:
import { createAdapter } from '@constela/start'; const adapter = createAdapter({ platform: 'cloudflare', // 'vercel' | 'deno' | 'node' routes: scannedRoutes, }); export default { fetch: adapter.fetch };
Packages
| Package | Version | Description |
|---|---|---|
create-constela |
0.2.3 | CLI scaffolding tool (npx create-constela my-app) |
@constela/core |
0.21.4 | AST types, JSON Schema, validator, 47 type guards, error codes, Style System |
@constela/compiler |
0.15.20 | 3-pass compiler: validate → analyze → transform, Style analysis |
@constela/runtime |
5.0.6 | DOM renderer, hydration, reactive signals, Style evaluation |
@constela/router |
23.0.0 | History API routing, dynamic params, catch-all routes |
@constela/server |
17.0.1 | SSR with Shiki dual-theme syntax highlighting |
@constela/start |
1.9.28 | Dev server, build, SSG, MDX, layouts, API routes, edge adapters |
@constela/cli |
0.5.78 | CLI: compile, dev, build, start, validate, inspect commands |
@constela/builder |
0.2.34 | Type-safe builders for programmatic AST construction |
@constela/ai |
6.0.4 | AI-powered DSL generation with Anthropic and OpenAI providers |
@constela/ui |
0.6.5 | Pre-built UI component library (30+ components) |
See each package's README for detailed API documentation.
Documentation
- Widget Integration Guide - Embedding independent Constela programs in pages
- Architecture - Package structure, compilation pipeline, and runtime
- Troubleshooting - Common errors, debugging, and solutions
CLI Usage
# Compile a Constela program constela compile app.constela.json # With custom output path constela compile app.constela.json --out dist/app.compiled.json # Pretty-print output constela compile app.constela.json --pretty # JSON output for AI tools constela compile app.constela.json --json # Watch mode - recompile on file changes constela compile app.constela.json --watch # Verbose output with timing constela compile app.constela.json --verbose # Fast validation without compilation constela validate app.constela.json constela validate --all src/routes/ # Inspect program structure constela inspect app.constela.json constela inspect app.constela.json --state --json
Error Messages with Suggestions
The CLI provides helpful error messages with "Did you mean?" suggestions:
Error [UNDEFINED_STATE] at /view/children/0/value/name
Undefined state reference: 'count'
Did you mean 'counter'?
API Usage
import { compile } from '@constela/compiler'; import { createApp } from '@constela/runtime'; // Load and compile const ast = JSON.parse(await fs.readFile('app.constela.json', 'utf-8')); const result = compile(ast); if (!result.ok) { console.error('Compilation failed:', result.errors); process.exit(1); } // Mount to DOM const app = createApp(result.program, document.getElementById('app')); // Later: cleanup app.destroy();
App Instance API
The createApp function returns an AppInstance with the following methods:
interface AppInstance { destroy(): void; // Cleanup and unmount setState(name: string, value: unknown): void; // Update state getState(name: string): unknown; // Read state subscribe(name: string, fn: (value: unknown) => void): () => void; // Observe state changes }
Example: External State Updates
const app = createApp(result.program, document.getElementById('app')); // Update state from outside the DSL app.setState('count', 10); // Subscribe to state changes const unsubscribe = app.subscribe('count', (value) => { console.log('Count changed:', value); }); // Read current state console.log(app.getState('count')); // 10 // Stop listening unsubscribe();
Server-Side Rendering (SSR)
Render Constela programs on the server with @constela/server:
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' }, }, });
Hydration
Hydrate server-rendered HTML on the client without DOM reconstruction:
import { hydrateApp } from '@constela/runtime'; const app = hydrateApp({ program: compiledProgram, mount: document.getElementById('app'), route: { params: { id: '123' }, query: new URLSearchParams('tab=overview'), path: '/users/123', }, imports: { config: { siteName: 'My Site' }, }, }); // App is now interactive app.subscribe('count', (value) => console.log('count:', value));
Error Model
All errors include structured information:
{ code: ErrorCode, message: string, path: string, // JSON Pointer, e.g., "/view/children/0/props/onClick" details?: object }
Error Codes:
SCHEMA_INVALID- JSON Schema validation errorUNDEFINED_STATE- Reference to undefined state fieldUNDEFINED_ACTION- Reference to undefined actionVAR_UNDEFINED- Reference to undefined variableDUPLICATE_ACTION- Duplicate action nameUNSUPPORTED_VERSION- Unsupported version stringCOMPONENT_NOT_FOUND- Reference to undefined componentCOMPONENT_PROP_MISSING- Required prop not providedCOMPONENT_CYCLE- Circular component reference detectedCOMPONENT_PROP_TYPE- Prop type mismatchPARAM_UNDEFINED- Reference to undefined param in componentOPERATION_INVALID_FOR_TYPE- Update operation incompatible with state typeOPERATION_MISSING_FIELD- Required field missing for update operationEXPR_COND_ELSE_REQUIRED- Cond expression requires else fieldUNDEFINED_ROUTE_PARAM- Route expression references undefined route paramUNDEFINED_IMPORT- Import expression references undefined importUNDEFINED_DATA- Data expression references undefined data sourceLAYOUT_MISSING_SLOT- Layout program has no slot nodeLAYOUT_NOT_FOUND- Referenced layout not found
Running Examples
# Install dependencies pnpm install # Build packages first pnpm build # Start examples dev server pnpm --filter @constela/examples dev # Then open in browser: # - http://localhost:5173/counter/ # - http://localhost:5173/todo-list/ # - http://localhost:5173/fetch-list/ # - http://localhost:5173/components/ # - http://localhost:5173/router/ # - http://localhost:5173/styles/
Design Principles
- Constrained surface area - Small set of node types and expression types
- Schema-first - DSL is JSON, validated by JSON Schema
- Compiler-first - Parse → validate → analyze → transform pipeline
- Deterministic state - Explicit declarations, no implicit reactivity
- AI-friendly errors - Structured errors with JSON Pointer paths
Development
# Install dependencies pnpm install # Build all packages pnpm build # Run tests pnpm test
Testing Guide
Running Tests
# Run all tests pnpm test # Run tests for specific package pnpm --filter @constela/core test pnpm --filter @constela/compiler test pnpm --filter @constela/runtime test pnpm --filter @constela/server test pnpm --filter @constela/router test pnpm --filter @constela/start test pnpm --filter @constela/cli test # Watch mode pnpm --filter @constela/core test -- --watch # Run specific test file pnpm --filter @constela/core test -- validator.test.ts
Test Structure
Each package has tests in tests/ or src/__tests__/:
| Package | Test Location | Coverage |
|---|---|---|
| @constela/core | tests/ |
Type guards, validators, error codes |
| @constela/compiler | tests/passes/ |
Validate, analyze, transform passes |
| @constela/runtime | src/__tests__/ |
Expression eval, action execution, hydration |
| @constela/server | src/__tests__/ |
SSR rendering, markdown, code blocks |
| @constela/router | tests/ |
Route matching, helpers, navigation |
| @constela/start | tests/ |
Build, dev server, data loading, layouts |
| @constela/cli | tests/ |
All CLI commands |
Writing Tests
Tests use Vitest. Follow the AAA pattern:
import { describe, it, expect } from 'vitest'; import { validateAst } from '../src/validator.js'; describe('validateAst', () => { it('should return ok for valid AST', () => { // Arrange const ast = { version: '1.0', state: {}, actions: [], view: { kind: 'element', tag: 'div' } }; // Act const result = validateAst(ast); // Assert expect(result.ok).toBe(true); }); });
Testing Constela Applications
To test your Constela application:
-
Validate JSON files:
constela validate --all src/routes/
-
Inspect program structure:
constela inspect src/routes/index.json --state
-
Build and check output:
constela build && ls -la dist/
License
MIT