@constela/runtime 0.10.3 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @constela/runtime
2
2
 
3
- Runtime DOM renderer for the Constela UI framework with fine-grained reactivity.
3
+ Executes Constela JSON programs in the browser with fine-grained reactivity.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,299 +8,157 @@ Runtime DOM renderer for the Constela UI framework with fine-grained reactivity.
8
8
  npm install @constela/runtime
9
9
  ```
10
10
 
11
- ## Overview
11
+ ## How It Works
12
12
 
13
- This package provides the client-side rendering engine for Constela applications. Key features:
13
+ Your JSON program:
14
14
 
15
- - **Fine-grained Reactivity** - Signal-based updates without virtual DOM
16
- - **Hydration** - Rehydrate server-rendered HTML
17
- - **Markdown & Code** - Built-in Markdown and syntax highlighting support
18
-
19
- ## API Reference
20
-
21
- ### createApp
22
-
23
- Creates and mounts a Constela application.
24
-
25
- ```typescript
26
- import { createApp } from '@constela/runtime';
27
-
28
- const app = createApp(compiledProgram, document.getElementById('app'));
29
-
30
- // Later: cleanup
31
- app.destroy();
32
- ```
33
-
34
- **Parameters:**
35
- - `program: CompiledProgram` - Compiled program from `@constela/compiler`
36
- - `mount: HTMLElement` - DOM element to mount to
37
-
38
- **Returns:** `AppInstance`
39
-
40
- ### hydrateApp
41
-
42
- Hydrates server-rendered HTML without DOM reconstruction.
43
-
44
- ```typescript
45
- import { hydrateApp } from '@constela/runtime';
46
-
47
- const app = hydrateApp({
48
- program: compiledProgram,
49
- mount: document.getElementById('app'),
50
- route: {
51
- params: { id: '123' },
52
- query: { tab: 'details' },
53
- path: '/users/123'
15
+ ```json
16
+ {
17
+ "version": "1.0",
18
+ "state": {
19
+ "count": { "type": "number", "initial": 0 }
54
20
  },
55
- imports: {
56
- config: { apiUrl: 'https://api.example.com' }
21
+ "actions": [
22
+ {
23
+ "name": "increment",
24
+ "steps": [{ "do": "update", "target": "count", "operation": "increment" }]
25
+ }
26
+ ],
27
+ "view": {
28
+ "kind": "element",
29
+ "tag": "button",
30
+ "props": { "onClick": { "event": "click", "action": "increment" } },
31
+ "children": [{ "kind": "text", "value": { "expr": "state", "name": "count" } }]
57
32
  }
58
- });
59
- ```
60
-
61
- **HydrateOptions:**
62
- - `program: CompiledProgram` - Compiled program
63
- - `mount: HTMLElement` - Container element
64
- - `route?: RouteContext` - Route parameters
65
- - `imports?: Record<string, unknown>` - Import data
66
-
67
- ### AppInstance
68
-
69
- ```typescript
70
- interface AppInstance {
71
- destroy(): void;
72
- setState(name: string, value: unknown): void;
73
- getState(name: string): unknown;
74
- subscribe(name: string, fn: (value: unknown) => void): () => void;
75
33
  }
76
34
  ```
77
35
 
78
- #### destroy()
79
-
80
- Cleans up the application, removes event listeners, and clears state.
36
+ Becomes an interactive app with:
37
+ - **Reactive state management** - Signal-based updates without virtual DOM
38
+ - **Efficient DOM updates** - Fine-grained reactivity
39
+ - **Event handling** - Declarative action binding
81
40
 
82
- #### setState(name, value)
41
+ ## Features
83
42
 
84
- Updates a state field programmatically.
43
+ ### Markdown Rendering
85
44
 
86
- ```typescript
87
- app.setState('count', 10);
88
- app.setState('user', { name: 'John', email: 'john@example.com' });
45
+ ```json
46
+ {
47
+ "kind": "markdown",
48
+ "content": { "expr": "state", "name": "markdownContent" }
49
+ }
89
50
  ```
90
51
 
91
- #### getState(name)
52
+ Rendered with [marked](https://marked.js.org/) and sanitized with [DOMPurify](https://github.com/cure53/DOMPurify).
92
53
 
93
- Reads current state value.
54
+ ### Code Blocks
94
55
 
95
- ```typescript
96
- const count = app.getState('count');
56
+ ```json
57
+ {
58
+ "kind": "code",
59
+ "code": { "expr": "lit", "value": "const x: number = 42;" },
60
+ "language": { "expr": "lit", "value": "typescript" }
61
+ }
97
62
  ```
98
63
 
99
- #### subscribe(name, fn)
64
+ Features:
65
+ - Syntax highlighting with [Shiki](https://shiki.style/)
66
+ - Dual theme support (light/dark)
67
+ - Built-in copy button
100
68
 
101
- Subscribes to state changes. Returns an unsubscribe function.
69
+ ### Hydration
102
70
 
103
- ```typescript
104
- const unsubscribe = app.subscribe('count', (value) => {
105
- console.log('count changed:', value);
106
- });
71
+ Server-rendered HTML is hydrated on the client without DOM reconstruction:
107
72
 
108
- // Later: stop listening
109
- unsubscribe();
73
+ ```json
74
+ {
75
+ "version": "1.0",
76
+ "state": { "theme": { "type": "string", "initial": "light" } },
77
+ "lifecycle": {
78
+ "onMount": "loadTheme"
79
+ },
80
+ "actions": [
81
+ {
82
+ "name": "loadTheme",
83
+ "steps": [
84
+ {
85
+ "do": "storage",
86
+ "operation": "get",
87
+ "key": { "expr": "lit", "value": "theme" },
88
+ "storage": "local",
89
+ "result": "savedTheme",
90
+ "onSuccess": [
91
+ { "do": "set", "target": "theme", "value": { "expr": "var", "name": "savedTheme" } }
92
+ ]
93
+ }
94
+ ]
95
+ }
96
+ ],
97
+ "view": { ... }
98
+ }
110
99
  ```
111
100
 
112
- ## Reactive Primitives
113
-
114
- Low-level reactive APIs for advanced usage.
115
-
116
- ### createSignal
117
-
118
- Creates a reactive signal with fine-grained dependency tracking.
119
-
120
- ```typescript
121
- import { createSignal } from '@constela/runtime';
122
-
123
- const count = createSignal(0);
101
+ ## Security
124
102
 
125
- // Read value (auto-tracks in effects)
126
- console.log(count.get()); // 0
103
+ The runtime includes security measures:
127
104
 
128
- // Update value
129
- count.set(1);
105
+ - **Prototype Pollution Prevention** - Blocks `__proto__`, `constructor`, `prototype`
106
+ - **Safe Globals** - Only exposes `JSON`, `Math`, `Date`, `Object`, `Array`, `String`, `Number`, `Boolean`, `console`
107
+ - **HTML Sanitization** - DOMPurify for Markdown content
130
108
 
131
- // Subscribe to changes
132
- const unsubscribe = count.subscribe((value) => {
133
- console.log('Value:', value);
134
- });
135
- ```
109
+ ## Internal API
136
110
 
137
- ### createEffect
111
+ > For framework developers only. End users should use the CLI.
138
112
 
139
- Creates a reactive side effect that auto-tracks dependencies.
113
+ ### createApp
140
114
 
141
115
  ```typescript
142
- import { createSignal, createEffect } from '@constela/runtime';
143
-
144
- const name = createSignal('World');
145
-
146
- const cleanup = createEffect(() => {
147
- console.log(`Hello, ${name.get()}!`);
148
-
149
- // Optional cleanup function
150
- return () => {
151
- console.log('Effect cleaned up');
152
- };
153
- });
116
+ import { createApp } from '@constela/runtime';
154
117
 
155
- name.set('Constela'); // Logs: "Hello, Constela!"
118
+ const app = createApp(compiledProgram, document.getElementById('app'));
156
119
 
157
- cleanup(); // Stops the effect
120
+ // Cleanup
121
+ app.destroy();
158
122
  ```
159
123
 
160
- ### createStateStore
161
-
162
- Centralized state management with signal-based reactivity.
124
+ ### hydrateApp
163
125
 
164
126
  ```typescript
165
- import { createStateStore } from '@constela/runtime';
127
+ import { hydrateApp } from '@constela/runtime';
166
128
 
167
- const store = createStateStore({
168
- count: { type: 'number', initial: 0 },
169
- name: { type: 'string', initial: '' }
129
+ const app = hydrateApp({
130
+ program: compiledProgram,
131
+ mount: document.getElementById('app'),
132
+ route: { params: { id: '123' }, query: new URLSearchParams(), path: '/users/123' },
133
+ imports: { config: { apiUrl: 'https://api.example.com' } }
170
134
  });
171
-
172
- store.get('count'); // 0
173
- store.set('count', 5);
174
- store.subscribe('count', (value) => console.log(value));
175
135
  ```
176
136
 
177
- ## Expression Evaluation
178
-
179
- ### evaluate
180
-
181
- Evaluates compiled expressions.
137
+ ### AppInstance
182
138
 
183
139
  ```typescript
184
- import { evaluate } from '@constela/runtime';
185
-
186
- const result = evaluate(expression, {
187
- state: stateStore,
188
- locals: { item: { id: 1, name: 'Test' } },
189
- route: { params: { id: '123' }, query: new URLSearchParams(), path: '/items/123' },
190
- imports: { config: { apiUrl: '...' } },
191
- data: { posts: [...] },
192
- refs: { inputEl: document.querySelector('#input') }
193
- });
140
+ interface AppInstance {
141
+ destroy(): void;
142
+ setState(name: string, value: unknown): void;
143
+ getState(name: string): unknown;
144
+ subscribe(name: string, fn: (value: unknown) => void): () => void;
145
+ }
194
146
  ```
195
147
 
196
- **Supported Expressions:**
197
- - Literals, state reads, variables
198
- - Binary operations, logical not, conditionals
199
- - Property access, array indexing
200
- - Route parameters/query/path
201
- - Imports and loaded data
202
- - DOM refs
203
-
204
- ## Action Execution
205
-
206
- ### executeAction
207
-
208
- Executes compiled actions.
148
+ ### Reactive Primitives
209
149
 
210
150
  ```typescript
211
- import { executeAction } from '@constela/runtime';
212
-
213
- await executeAction(action, {
214
- state: stateStore,
215
- locals: {},
216
- route: { ... },
217
- imports: { ... },
218
- refs: { ... },
219
- subscriptions: []
220
- });
221
- ```
222
-
223
- **Supported Steps:**
224
- - `set`, `update` - State mutations
225
- - `fetch` - HTTP requests
226
- - `storage` - localStorage/sessionStorage
227
- - `clipboard` - Clipboard operations
228
- - `navigate` - Page navigation
229
- - `import`, `call` - Dynamic imports and function calls
230
- - `subscribe`, `dispose` - Event subscriptions
231
- - `dom` - DOM manipulation
232
- - `if` - Conditional execution
233
-
234
- ## Rendering
235
-
236
- ### render
151
+ import { createSignal, createEffect } from '@constela/runtime';
237
152
 
238
- Renders compiled nodes to DOM.
153
+ const count = createSignal(0);
154
+ count.get(); // Read
155
+ count.set(1); // Write
239
156
 
240
- ```typescript
241
- import { render } from '@constela/runtime';
242
-
243
- const domNode = render(compiledNode, {
244
- state: stateStore,
245
- actions: compiledProgram.actions,
246
- components: {},
247
- locals: {},
248
- route: { ... },
249
- imports: { ... },
250
- refs: {},
251
- subscriptions: [],
252
- cleanups: []
157
+ const cleanup = createEffect(() => {
158
+ console.log(`Count: ${count.get()}`);
253
159
  });
254
160
  ```
255
161
 
256
- **Supported Nodes:**
257
- - Element nodes with props and event handlers
258
- - Text nodes with reactive updates
259
- - Conditional rendering (`if/else`)
260
- - List rendering (`each`)
261
- - Markdown with sanitization
262
- - Code blocks with Shiki highlighting
263
-
264
- ## Markdown & Code Blocks
265
-
266
- The runtime includes built-in support for rendering Markdown and syntax-highlighted code.
267
-
268
- ### Markdown
269
-
270
- Rendered using [marked](https://marked.js.org/) with [DOMPurify](https://github.com/cure53/DOMPurify) sanitization.
271
-
272
- ```json
273
- {
274
- "kind": "markdown",
275
- "content": { "expr": "state", "name": "markdownContent" }
276
- }
277
- ```
278
-
279
- ### Code Blocks
280
-
281
- Rendered with [Shiki](https://shiki.style/) syntax highlighting.
282
-
283
- ```json
284
- {
285
- "kind": "code",
286
- "code": { "expr": "lit", "value": "const x = 1;" },
287
- "language": { "expr": "lit", "value": "typescript" }
288
- }
289
- ```
290
-
291
- **Features:**
292
- - Dual theme support (light/dark)
293
- - Copy button with feedback
294
- - Dynamic language loading
295
-
296
- ## Security
297
-
298
- The runtime includes security measures:
299
-
300
- - **Prototype Pollution Prevention** - Blocks `__proto__`, `constructor`, `prototype`
301
- - **Safe Globals** - Only exposes `JSON`, `Math`, `Date`, `Object`, `Array`, `String`, `Number`, `Boolean`, `console`
302
- - **HTML Sanitization** - DOMPurify for Markdown content
303
-
304
162
  ## License
305
163
 
306
164
  MIT
package/dist/index.d.ts CHANGED
@@ -47,6 +47,17 @@ declare function createStateStore(definitions: Record<string, StateDefinition>):
47
47
  * - Property access (get)
48
48
  */
49
49
 
50
+ /**
51
+ * Style preset definition - matches @constela/core StylePreset
52
+ */
53
+ interface StylePreset {
54
+ base: string;
55
+ variants?: Record<string, Record<string, string>>;
56
+ defaultVariants?: Record<string, string>;
57
+ compoundVariants?: Array<Record<string, string> & {
58
+ class: string;
59
+ }>;
60
+ }
50
61
  interface EvaluationContext {
51
62
  state: StateStore;
52
63
  locals: Record<string, unknown>;
@@ -57,8 +68,25 @@ interface EvaluationContext {
57
68
  };
58
69
  imports?: Record<string, unknown>;
59
70
  refs?: Record<string, Element>;
71
+ styles?: Record<string, StylePreset>;
60
72
  }
61
73
  declare function evaluate(expr: CompiledExpression, ctx: EvaluationContext): unknown;
74
+ /**
75
+ * Style expression type for evaluateStyle
76
+ */
77
+ interface StyleExprInput {
78
+ expr: 'style';
79
+ name: string;
80
+ variants?: Record<string, CompiledExpression>;
81
+ }
82
+ /**
83
+ * Evaluates a style expression to produce CSS class names
84
+ *
85
+ * @param expr - The style expression to evaluate
86
+ * @param ctx - The evaluation context containing styles presets
87
+ * @returns The computed CSS class string, or undefined if preset not found
88
+ */
89
+ declare function evaluateStyle(expr: StyleExprInput, ctx: EvaluationContext): string | undefined;
62
90
 
63
91
  /**
64
92
  * Action Executor - Executes compiled action steps
@@ -175,4 +203,4 @@ interface HydrateOptions {
175
203
  */
176
204
  declare function hydrateApp(options: HydrateOptions): AppInstance;
177
205
 
178
- export { type ActionContext, type AppInstance, type EvaluationContext, type HydrateOptions, type RenderContext, type Signal, type StateStore, createApp, createEffect, createSignal, createStateStore, evaluate, executeAction, hydrateApp, render };
206
+ export { type ActionContext, type AppInstance, type EvaluationContext, type HydrateOptions, type RenderContext, type Signal, type StateStore, type StylePreset, createApp, createEffect, createSignal, createStateStore, evaluate, evaluateStyle, executeAction, hydrateApp, render };
package/dist/index.js CHANGED
@@ -99,7 +99,21 @@ function createEffect(fn) {
99
99
  function createStateStore(definitions) {
100
100
  const signals = /* @__PURE__ */ new Map();
101
101
  for (const [name, def] of Object.entries(definitions)) {
102
- signals.set(name, createSignal(def.initial));
102
+ let initialValue = def.initial;
103
+ if (name === "theme" && typeof window !== "undefined") {
104
+ try {
105
+ const stored = localStorage.getItem("theme");
106
+ if (stored !== null) {
107
+ try {
108
+ initialValue = JSON.parse(stored);
109
+ } catch {
110
+ initialValue = stored;
111
+ }
112
+ }
113
+ } catch {
114
+ }
115
+ }
116
+ signals.set(name, createSignal(initialValue));
103
117
  }
104
118
  return {
105
119
  get(name) {
@@ -251,6 +265,8 @@ function evaluate(expr, ctx) {
251
265
  case "param": {
252
266
  return void 0;
253
267
  }
268
+ case "style":
269
+ return evaluateStyle(expr, ctx);
254
270
  default: {
255
271
  const _exhaustiveCheck = expr;
256
272
  throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustiveCheck)}`);
@@ -349,6 +365,36 @@ function evaluateBinary(op, left, right, ctx) {
349
365
  throw new Error("Unknown binary operator: " + op);
350
366
  }
351
367
  }
368
+ function evaluateStyle(expr, ctx) {
369
+ const preset = ctx.styles?.[expr.name];
370
+ if (!preset) return void 0;
371
+ let classes = preset.base;
372
+ if (preset.variants) {
373
+ for (const variantKey of Object.keys(preset.variants)) {
374
+ let variantValueStr = null;
375
+ if (expr.variants?.[variantKey]) {
376
+ let variantValue;
377
+ try {
378
+ variantValue = evaluate(expr.variants[variantKey], ctx);
379
+ } catch {
380
+ continue;
381
+ }
382
+ if (variantValue != null) {
383
+ variantValueStr = String(variantValue);
384
+ }
385
+ } else if (preset.defaultVariants?.[variantKey] !== void 0) {
386
+ variantValueStr = preset.defaultVariants[variantKey];
387
+ }
388
+ if (variantValueStr !== null) {
389
+ const variantClasses = preset.variants[variantKey]?.[variantValueStr];
390
+ if (variantClasses) {
391
+ classes += " " + variantClasses;
392
+ }
393
+ }
394
+ }
395
+ }
396
+ return classes.trim();
397
+ }
352
398
 
353
399
  // src/action/executor.ts
354
400
  function createEvalContext(ctx) {
@@ -13769,6 +13815,7 @@ export {
13769
13815
  createSignal,
13770
13816
  createStateStore,
13771
13817
  evaluate,
13818
+ evaluateStyle,
13772
13819
  executeAction,
13773
13820
  hydrateApp,
13774
13821
  render
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/runtime",
3
- "version": "0.10.3",
3
+ "version": "0.11.1",
4
4
  "description": "Runtime DOM renderer for Constela UI framework",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,8 +18,8 @@
18
18
  "dompurify": "^3.3.1",
19
19
  "marked": "^17.0.1",
20
20
  "shiki": "^3.20.0",
21
- "@constela/compiler": "0.7.1",
22
- "@constela/core": "0.7.0"
21
+ "@constela/core": "0.8.0",
22
+ "@constela/compiler": "0.8.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/dompurify": "^3.2.0",
@@ -29,7 +29,7 @@
29
29
  "tsup": "^8.0.0",
30
30
  "typescript": "^5.3.0",
31
31
  "vitest": "^2.0.0",
32
- "@constela/server": "3.0.1"
32
+ "@constela/server": "4.1.0"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=20.0.0"