@constela/runtime 0.12.0 → 0.12.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 +102 -1
- package/dist/index.js +30 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -40,6 +40,81 @@ 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
|
+
### Key-based List Diffing
|
|
69
|
+
|
|
70
|
+
Efficient list updates - only changed items re-render:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"kind": "each",
|
|
75
|
+
"items": { "expr": "state", "name": "posts" },
|
|
76
|
+
"as": "post",
|
|
77
|
+
"key": { "expr": "var", "name": "post", "path": "id" },
|
|
78
|
+
"body": { ... }
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Benefits:
|
|
83
|
+
- Add/remove items: Only affected DOM nodes change
|
|
84
|
+
- Reorder: DOM nodes move without recreation
|
|
85
|
+
- Update item: Only that item re-renders
|
|
86
|
+
- Input state preserved during updates
|
|
87
|
+
|
|
88
|
+
### WebSocket Connections
|
|
89
|
+
|
|
90
|
+
Real-time data with declarative WebSocket:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"connections": {
|
|
95
|
+
"chat": {
|
|
96
|
+
"type": "websocket",
|
|
97
|
+
"url": "wss://api.example.com/ws",
|
|
98
|
+
"onMessage": { "action": "handleMessage" },
|
|
99
|
+
"onOpen": { "action": "connectionOpened" },
|
|
100
|
+
"onClose": { "action": "connectionClosed" }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Send messages:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{ "do": "send", "connection": "chat", "data": { "expr": "state", "name": "inputText" } }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Close connection:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{ "do": "close", "connection": "chat" }
|
|
116
|
+
```
|
|
117
|
+
|
|
43
118
|
### Markdown Rendering
|
|
44
119
|
|
|
45
120
|
```json
|
|
@@ -148,17 +223,43 @@ interface AppInstance {
|
|
|
148
223
|
### Reactive Primitives
|
|
149
224
|
|
|
150
225
|
```typescript
|
|
151
|
-
import { createSignal, createEffect } from '@constela/runtime';
|
|
226
|
+
import { createSignal, createEffect, createComputed } from '@constela/runtime';
|
|
152
227
|
|
|
153
228
|
const count = createSignal(0);
|
|
154
229
|
count.get(); // Read
|
|
155
230
|
count.set(1); // Write
|
|
156
231
|
|
|
232
|
+
// Computed values with automatic dependency tracking
|
|
233
|
+
const doubled = createComputed(() => count.get() * 2);
|
|
234
|
+
doubled.get(); // Returns memoized value
|
|
235
|
+
|
|
157
236
|
const cleanup = createEffect(() => {
|
|
158
237
|
console.log(`Count: ${count.get()}`);
|
|
159
238
|
});
|
|
160
239
|
```
|
|
161
240
|
|
|
241
|
+
### TypedStateStore (TypeScript)
|
|
242
|
+
|
|
243
|
+
Type-safe state access for TypeScript developers:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
import { createTypedStateStore } from '@constela/runtime';
|
|
247
|
+
|
|
248
|
+
interface AppState {
|
|
249
|
+
posts: { id: number; liked: boolean }[];
|
|
250
|
+
filter: string;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const state = createTypedStateStore<AppState>({
|
|
254
|
+
posts: { type: 'list', initial: [] },
|
|
255
|
+
filter: { type: 'string', initial: '' },
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
state.get('posts'); // Type: { id: number; liked: boolean }[]
|
|
259
|
+
state.set('filter', 'recent'); // OK
|
|
260
|
+
state.set('filter', 123); // TypeScript error
|
|
261
|
+
```
|
|
262
|
+
|
|
162
263
|
## License
|
|
163
264
|
|
|
164
265
|
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 }
|
|
@@ -13711,7 +13739,7 @@ function hydrateElement(node, el, ctx) {
|
|
|
13711
13739
|
}
|
|
13712
13740
|
let payload = void 0;
|
|
13713
13741
|
if (handler.payload) {
|
|
13714
|
-
payload =
|
|
13742
|
+
payload = evaluatePayload(handler.payload, {
|
|
13715
13743
|
state: ctx.state,
|
|
13716
13744
|
locals: { ...ctx.locals, ...eventLocals },
|
|
13717
13745
|
...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.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.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"
|