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
| 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 |
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.
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.
# Recommended: Full-stack development
npm install @constela/start
# Low-level API (advanced)
npm install @constela/runtime @constela/compiler- 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:
npx constela devOpen http://localhost:3000 to see your app.
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.
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
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
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
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"] } }// 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
// 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
// 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
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
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.
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
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" }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
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) |
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" }.
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
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">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)
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" }.
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
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" } }
]
}
]
}
]
}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" }{
"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": "+" } }]
}
]
}
}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.
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', ... }
};@constela/start provides a complete framework for building Constela applications:
Configure your application via constela.config.json:
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.
# Start development server
npx constela dev --port 3000Features:
- Vite-powered hot reload
- File-based routing
- Layout composition
- SSR with hydration
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.
# Build for production
npx constela build --outDir distGenerates:
- Static HTML for all routes
- Bundled runtime JavaScript
- Optimized assets
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
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
};Add request middleware:
// pages/_middleware.ts
export default async (ctx, next) => {
console.log('Request:', ctx.url);
const response = await next();
return response;
};Deploy to edge platforms:
import { createAdapter } from '@constela/start';
const adapter = createAdapter({
platform: 'cloudflare', // 'vercel' | 'deno' | 'node'
routes: scannedRoutes,
});
export default { fetch: adapter.fetch };| 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.
- Widget Integration Guide - Embedding independent Constela programs in pages
- Architecture - Package structure, compilation pipeline, and runtime
- Troubleshooting - Common errors, debugging, and solutions
# 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 --jsonThe 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'?
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();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
}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();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' },
},
});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));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
# 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/- 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
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Run tests
pnpm test# 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.tsEach 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 |
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);
});
});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/
MIT