@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.
- package/README.md +150 -1
- package/dist/index.js +54 -2
- 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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
22
|
-
"@constela/core": "0.9.
|
|
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.
|
|
32
|
+
"@constela/server": "5.0.1"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
35
35
|
"node": ">=20.0.0"
|