@constela/runtime 0.12.0 → 0.12.2

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.
Files changed (3) hide show
  1. package/README.md +150 -1
  2. package/dist/index.js +54 -2
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -40,6 +40,129 @@ Becomes an interactive app with:
40
40
 
41
41
  ## Features
42
42
 
43
+ ### Fine-grained State Updates (setPath)
44
+
45
+ Update nested values without replacing entire arrays:
46
+
47
+ ```json
48
+ {
49
+ "do": "setPath",
50
+ "target": "posts",
51
+ "path": [5, "liked"],
52
+ "value": { "expr": "lit", "value": true }
53
+ }
54
+ ```
55
+
56
+ Dynamic path with variables:
57
+
58
+ ```json
59
+ {
60
+ "do": "setPath",
61
+ "target": "posts",
62
+ "path": { "expr": "var", "name": "payload", "path": "index" },
63
+ "field": "liked",
64
+ "value": { "expr": "lit", "value": true }
65
+ }
66
+ ```
67
+
68
+ ### String Concatenation (concat)
69
+
70
+ Build dynamic strings from multiple expressions:
71
+
72
+ ```json
73
+ {
74
+ "expr": "concat",
75
+ "items": [
76
+ { "expr": "lit", "value": "/users/" },
77
+ { "expr": "var", "name": "userId" },
78
+ { "expr": "lit", "value": "/profile" }
79
+ ]
80
+ }
81
+ ```
82
+
83
+ Useful for:
84
+ - Dynamic URLs: `/api/posts/{id}`
85
+ - CSS class names: `btn btn-{variant}`
86
+ - Formatted messages: `Hello, {name}!`
87
+
88
+ ### Object Payloads for Event Handlers
89
+
90
+ Pass multiple values to actions with object-shaped payloads:
91
+
92
+ ```json
93
+ {
94
+ "kind": "element",
95
+ "tag": "button",
96
+ "props": {
97
+ "onClick": {
98
+ "event": "click",
99
+ "action": "toggleLike",
100
+ "payload": {
101
+ "index": { "expr": "var", "name": "index" },
102
+ "postId": { "expr": "var", "name": "post", "path": "id" },
103
+ "currentLiked": { "expr": "var", "name": "post", "path": "liked" }
104
+ }
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ Each expression field in the payload is evaluated when the event fires. The action receives the evaluated object:
111
+
112
+ ```json
113
+ { "index": 5, "postId": "abc123", "currentLiked": true }
114
+ ```
115
+
116
+ ### Key-based List Diffing
117
+
118
+ Efficient list updates - only changed items re-render:
119
+
120
+ ```json
121
+ {
122
+ "kind": "each",
123
+ "items": { "expr": "state", "name": "posts" },
124
+ "as": "post",
125
+ "key": { "expr": "var", "name": "post", "path": "id" },
126
+ "body": { ... }
127
+ }
128
+ ```
129
+
130
+ Benefits:
131
+ - Add/remove items: Only affected DOM nodes change
132
+ - Reorder: DOM nodes move without recreation
133
+ - Update item: Only that item re-renders
134
+ - Input state preserved during updates
135
+
136
+ ### WebSocket Connections
137
+
138
+ Real-time data with declarative WebSocket:
139
+
140
+ ```json
141
+ {
142
+ "connections": {
143
+ "chat": {
144
+ "type": "websocket",
145
+ "url": "wss://api.example.com/ws",
146
+ "onMessage": { "action": "handleMessage" },
147
+ "onOpen": { "action": "connectionOpened" },
148
+ "onClose": { "action": "connectionClosed" }
149
+ }
150
+ }
151
+ }
152
+ ```
153
+
154
+ Send messages:
155
+
156
+ ```json
157
+ { "do": "send", "connection": "chat", "data": { "expr": "state", "name": "inputText" } }
158
+ ```
159
+
160
+ Close connection:
161
+
162
+ ```json
163
+ { "do": "close", "connection": "chat" }
164
+ ```
165
+
43
166
  ### Markdown Rendering
44
167
 
45
168
  ```json
@@ -148,17 +271,43 @@ interface AppInstance {
148
271
  ### Reactive Primitives
149
272
 
150
273
  ```typescript
151
- import { createSignal, createEffect } from '@constela/runtime';
274
+ import { createSignal, createEffect, createComputed } from '@constela/runtime';
152
275
 
153
276
  const count = createSignal(0);
154
277
  count.get(); // Read
155
278
  count.set(1); // Write
156
279
 
280
+ // Computed values with automatic dependency tracking
281
+ const doubled = createComputed(() => count.get() * 2);
282
+ doubled.get(); // Returns memoized value
283
+
157
284
  const cleanup = createEffect(() => {
158
285
  console.log(`Count: ${count.get()}`);
159
286
  });
160
287
  ```
161
288
 
289
+ ### TypedStateStore (TypeScript)
290
+
291
+ Type-safe state access for TypeScript developers:
292
+
293
+ ```typescript
294
+ import { createTypedStateStore } from '@constela/runtime';
295
+
296
+ interface AppState {
297
+ posts: { id: number; liked: boolean }[];
298
+ filter: string;
299
+ }
300
+
301
+ const state = createTypedStateStore<AppState>({
302
+ posts: { type: 'list', initial: [] },
303
+ filter: { type: 'string', initial: '' },
304
+ });
305
+
306
+ state.get('posts'); // Type: { id: number; liked: boolean }[]
307
+ state.set('filter', 'recent'); // OK
308
+ state.set('filter', 123); // TypeScript error
309
+ ```
310
+
162
311
  ## License
163
312
 
164
313
  MIT
package/dist/index.js CHANGED
@@ -422,12 +422,40 @@ function evaluate(expr, ctx) {
422
422
  }
423
423
  case "style":
424
424
  return evaluateStyle(expr, ctx);
425
+ case "concat": {
426
+ return expr.items.map((item) => {
427
+ const val = evaluate(item, ctx);
428
+ return val == null ? "" : String(val);
429
+ }).join("");
430
+ }
425
431
  default: {
426
432
  const _exhaustiveCheck = expr;
427
433
  throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustiveCheck)}`);
428
434
  }
429
435
  }
430
436
  }
437
+ function isExpression(value) {
438
+ return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, "expr") && typeof value.expr === "string";
439
+ }
440
+ function evaluatePayload(payload, ctx) {
441
+ if (isExpression(payload)) {
442
+ return evaluate(payload, ctx);
443
+ }
444
+ if (typeof payload === "object" && payload !== null) {
445
+ const forbiddenKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
446
+ const result = {};
447
+ for (const [key2, value] of Object.entries(payload)) {
448
+ if (forbiddenKeys.has(key2)) continue;
449
+ if (isExpression(value)) {
450
+ result[key2] = evaluate(value, ctx);
451
+ } else {
452
+ result[key2] = value;
453
+ }
454
+ }
455
+ return result;
456
+ }
457
+ return payload;
458
+ }
431
459
  function getNestedValue(obj, path) {
432
460
  const forbiddenKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
433
461
  const parts = path.split(".");
@@ -13165,7 +13193,7 @@ function renderElement(node, ctx) {
13165
13193
  }
13166
13194
  let payload = void 0;
13167
13195
  if (handler.payload) {
13168
- payload = evaluate(handler.payload, {
13196
+ payload = evaluatePayload(handler.payload, {
13169
13197
  state: ctx.state,
13170
13198
  locals: { ...ctx.locals, ...eventLocals },
13171
13199
  ...ctx.imports && { imports: ctx.imports }
@@ -13314,6 +13342,18 @@ function createReactiveLocals(baseLocals, itemKey, itemSignal, indexKey, indexSi
13314
13342
  if (prop === itemKey) return true;
13315
13343
  if (indexKey && prop === indexKey) return true;
13316
13344
  return prop in target;
13345
+ },
13346
+ ownKeys(target) {
13347
+ const keys = Reflect.ownKeys(target);
13348
+ if (!keys.includes(itemKey)) keys.push(itemKey);
13349
+ if (indexKey && !keys.includes(indexKey)) keys.push(indexKey);
13350
+ return keys;
13351
+ },
13352
+ getOwnPropertyDescriptor(target, prop) {
13353
+ if (prop === itemKey || indexKey && prop === indexKey) {
13354
+ return { enumerable: true, configurable: true };
13355
+ }
13356
+ return Reflect.getOwnPropertyDescriptor(target, prop);
13317
13357
  }
13318
13358
  });
13319
13359
  }
@@ -13588,6 +13628,18 @@ function createReactiveLocals2(baseLocals, itemSignal, indexSignal, itemName, in
13588
13628
  if (prop === itemName) return true;
13589
13629
  if (indexName && prop === indexName) return true;
13590
13630
  return prop in target;
13631
+ },
13632
+ ownKeys(target) {
13633
+ const keys = Reflect.ownKeys(target);
13634
+ if (!keys.includes(itemName)) keys.push(itemName);
13635
+ if (indexName && !keys.includes(indexName)) keys.push(indexName);
13636
+ return keys;
13637
+ },
13638
+ getOwnPropertyDescriptor(target, prop) {
13639
+ if (prop === itemName || indexName && prop === indexName) {
13640
+ return { enumerable: true, configurable: true };
13641
+ }
13642
+ return Reflect.getOwnPropertyDescriptor(target, prop);
13591
13643
  }
13592
13644
  });
13593
13645
  }
@@ -13711,7 +13763,7 @@ function hydrateElement(node, el, ctx) {
13711
13763
  }
13712
13764
  let payload = void 0;
13713
13765
  if (handler.payload) {
13714
- payload = evaluate(handler.payload, {
13766
+ payload = evaluatePayload(handler.payload, {
13715
13767
  state: ctx.state,
13716
13768
  locals: { ...ctx.locals, ...eventLocals },
13717
13769
  ...ctx.imports && { imports: ctx.imports },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/runtime",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
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.9.0",
22
- "@constela/core": "0.9.0"
21
+ "@constela/compiler": "0.9.1",
22
+ "@constela/core": "0.9.1"
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": "5.0.0"
32
+ "@constela/server": "5.0.1"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=20.0.0"