@constela/start
v1.11.0
Published
Meta-framework for Constela applications
Readme
@constela/start
Meta-framework for Constela applications with dev server, build tools, and SSG.
Installation
npm install @constela/startQuick Start
- 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 development:
npx constela devOpen http://localhost:3000 to see your app.
CLI Usage
# Development server
npx constela dev --port 3000
# Production build
npx constela build --outDir dist
# Start production server
npx constela start --port 3000Project Structure
project/
src/
routes/ # Page files (.constela.json)
index.constela.json # / route
about.constela.json # /about route
users/
[id].constela.json # /users/:id route
layouts/ # Layout files
main.json
docs.json
components/ # Component files
header.json
footer.json
data/ # Data files
config.json
navigation.json
content/ # Content files (MDX)
blog/
*.mdx
public/ # Static assetsFile-Based Routing
| File | Route |
|------|-------|
| index.constela.json | / |
| about.constela.json | /about |
| users/[id].constela.json | /users/:id |
| docs/[...slug].constela.json | /docs/* |
Data Sources
Load data at build time:
Glob Pattern
{
"data": {
"posts": {
"type": "glob",
"pattern": "content/blog/*.mdx",
"transform": "mdx"
}
}
}File
{
"data": {
"config": {
"type": "file",
"path": "data/config.json"
}
}
}API
{
"data": {
"users": {
"type": "api",
"url": "https://api.example.com/users"
}
}
}Transforms: mdx, yaml, csv
Layouts
Define reusable layouts:
{
"version": "1.0",
"type": "layout",
"state": {
"theme": { "type": "string", "initial": "light" }
},
"view": {
"kind": "element",
"tag": "div",
"children": [
{ "kind": "component", "name": "Header" },
{ "kind": "element", "tag": "main", "children": [{ "kind": "slot" }] },
{ "kind": "component", "name": "Footer" }
]
}
}Components with Local State in Layouts
Layout components can use localState and localActions for instance-scoped state:
{
"version": "1.0",
"type": "layout",
"components": {
"Sidebar": {
"params": { "items": { "type": "list" } },
"localState": {
"expandedCategory": { "type": "string", "initial": "" }
},
"localActions": [
{
"name": "toggleCategory",
"steps": [{ "do": "set", "target": "expandedCategory", "value": { "expr": "var", "name": "payload" } }]
}
],
"view": {
"kind": "each",
"items": { "expr": "param", "name": "items" },
"as": "item",
"body": {
"kind": "element",
"tag": "div",
"children": [
{
"kind": "element",
"tag": "button",
"props": {
"onClick": {
"event": "click",
"action": "toggleCategory",
"payload": { "expr": "var", "name": "item", "path": "name" }
}
},
"children": [
{ "kind": "text", "value": { "expr": "var", "name": "item", "path": "name" } }
]
}
]
}
}
}
},
"view": { ... }
}Key features:
- Components in layouts support
localStateandlocalActions paramexpressions inside components are substituted with prop values- Works with dynamic props from
eachloops - Each component instance maintains independent state
Use layout in pages:
{
"route": {
"path": "/docs/:slug",
"layout": "docs",
"layoutParams": {
"title": { "expr": "data", "name": "doc", "path": "title" }
}
}
}Named Slots:
{ "kind": "slot", "name": "sidebar" }Static Paths
Generate static pages for dynamic routes:
{
"data": {
"docs": {
"type": "glob",
"pattern": "content/docs/*.mdx",
"transform": "mdx"
}
},
"route": {
"path": "/docs/:slug",
"getStaticPaths": {
"source": "docs",
"params": {
"slug": { "expr": "get", "base": { "expr": "var", "name": "item" }, "path": "slug" }
}
}
}
}MDX Support
Markdown files with JSX components:
---
title: Getting Started
---
# Welcome
<Callout type="info">
This is an info callout.
</Callout>Security
MDX attribute expressions are validated at compile time. Dangerous patterns like require(), eval(), or window in actual code will throw explicit errors:
<!-- Error: MDX attribute contains disallowed pattern: require -->
<Button data={require("module")} />However, these words are allowed inside string literals:
<!-- OK: "require" is inside a string literal -->
<PropsTable items={[{ description: "operations that require one" }]} />Islands Architecture
Define interactive islands for partial hydration:
{
"view": {
"kind": "element",
"tag": "div",
"children": [
{
"kind": "island",
"id": "interactive-counter",
"strategy": "visible",
"strategyOptions": { "threshold": 0.5 },
"content": {
"kind": "element",
"tag": "button",
"props": { "onClick": { "event": "click", "action": "increment" } },
"children": [{ "kind": "text", "value": { "expr": "state", "name": "count" } }]
},
"state": {
"count": { "type": "number", "initial": 0 }
},
"actions": [
{
"name": "increment",
"steps": [{ "do": "update", "target": "count", "operation": "increment" }]
}
]
}
]
}
}Hydration Strategies:
| Strategy | When to Hydrate | Use Case |
|----------|-----------------|----------|
| load | Immediately | Critical interactive elements |
| idle | Browser idle | Non-urgent interactions |
| visible | In viewport | Below-the-fold content |
| interaction | User interaction | Lazy-loaded widgets |
| media | Media query match | Responsive components |
| never | Never | Static content only |
Build Optimization:
Islands are automatically code-split during build:
npx constela build --islandsOutput structure:
dist/
_islands/
interactive-counter.js # Island-specific bundle
chart-widget.js
client.js # Main client bundleConfiguration
Create constela.config.json:
{
"adapter": "node",
"css": "src/styles/globals.css",
"layoutsDir": "src/layouts",
"islands": {
"enabled": true,
"defaultStrategy": "visible"
},
"streaming": {
"enabled": true,
"flushStrategy": "batched"
}
}Adapters: cloudflare, vercel, deno, node
Internal API
For framework developers only. End users should use the CLI.
createDevServer
import { createDevServer } from '@constela/start';
const server = await createDevServer({
port: 3000,
routesDir: 'src/routes',
layoutsDir: 'src/layouts',
});build
import { build } from '@constela/start';
const result = await build({
outDir: 'dist',
routesDir: 'src/routes',
target: 'node',
});DataLoader
import { DataLoader } from '@constela/start';
const loader = new DataLoader('src/routes');
const posts = await loader.loadGlob('content/blog/*.mdx', 'mdx');
const config = await loader.loadFile('data/config.json');LayoutResolver
import { LayoutResolver } from '@constela/start';
const resolver = new LayoutResolver('src/layouts');
await resolver.scan();
const layout = await resolver.load('docs');API Routes
// src/routes/api/users.ts
export const GET = async (ctx) => {
return new Response(JSON.stringify({ users: [] }), {
headers: { 'Content-Type': 'application/json' },
});
};Middleware
// src/routes/_middleware.ts
export default async (ctx, next) => {
console.log('Request:', ctx.url);
return next();
};Edge Adapters
Deploy to any edge platform with streaming support:
import { createAdapter } from '@constela/start';
const adapter = createAdapter({
platform: 'cloudflare',
routes: scannedRoutes,
streaming: true,
});
export default { fetch: adapter.fetch };Platform-specific adapters:
// Cloudflare Workers
import { cloudflareAdapter } from '@constela/start/adapters/cloudflare';
export default {
fetch: cloudflareAdapter({
routes: scannedRoutes,
streaming: true,
}),
};
// Vercel Edge
import { vercelAdapter } from '@constela/start/adapters/vercel';
export const config = { runtime: 'edge' };
export default vercelAdapter({ routes: scannedRoutes, streaming: true });
// Deno Deploy
import { denoAdapter } from '@constela/start/adapters/deno';
Deno.serve(denoAdapter({ routes: scannedRoutes, streaming: true }));
// Node.js
import { nodeAdapter } from '@constela/start/adapters/node';
import { createServer } from 'http';
const handler = nodeAdapter({ routes: scannedRoutes, streaming: true });
createServer(handler).listen(3000);Streaming Response:
All adapters support streaming HTML responses:
// Returns ReadableStream<Uint8Array> for streaming
const response = await adapter.fetch(request);
// Content-Type: text/html; charset=utf-8
// Transfer-Encoding: chunkedLicense
MIT
