@constela/runtime 0.12.1 → 0.13.0

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
@@ -65,6 +65,54 @@ Dynamic path with variables:
65
65
  }
66
66
  ```
67
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
+
68
116
  ### Key-based List Diffing
69
117
 
70
118
  Efficient list updates - only changed items re-render:
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { CompiledExpression, CompiledAction, CompiledNode, CompiledProgram } from '@constela/compiler';
1
+ import { CompiledExpression, CompiledAction, CompiledNode, CompiledLocalAction, CompiledProgram } from '@constela/compiler';
2
2
 
3
3
  interface Signal<T> {
4
4
  get(): T;
@@ -254,6 +254,20 @@ declare function createConnectionManager(): ConnectionManager;
254
254
  * - navigate: Page navigation
255
255
  */
256
256
 
257
+ /**
258
+ * Local state store interface for component-level state
259
+ */
260
+ interface LocalStateStore$1 {
261
+ get(name: string): unknown;
262
+ set(name: string, value: unknown): void;
263
+ }
264
+ /**
265
+ * Extended action type with local action metadata
266
+ */
267
+ interface ExtendedAction extends CompiledAction {
268
+ _isLocalAction?: boolean;
269
+ _localStore?: LocalStateStore$1;
270
+ }
257
271
  interface ActionContext {
258
272
  state: StateStore;
259
273
  actions: Record<string, CompiledAction>;
@@ -269,7 +283,7 @@ interface ActionContext {
269
283
  imports?: Record<string, unknown>;
270
284
  connections?: ConnectionManager;
271
285
  }
272
- declare function executeAction(action: CompiledAction, ctx: ActionContext): Promise<void>;
286
+ declare function executeAction(action: CompiledAction | ExtendedAction, ctx: ActionContext): Promise<void>;
273
287
 
274
288
  /**
275
289
  * Renderer - DOM rendering for compiled view nodes
@@ -281,6 +295,14 @@ declare function executeAction(action: CompiledAction, ctx: ActionContext): Prom
281
295
  * - each: List rendering with reactive updates
282
296
  */
283
297
 
298
+ /**
299
+ * Local state store interface for component-level state
300
+ */
301
+ interface LocalStateStore {
302
+ get(name: string): unknown;
303
+ set(name: string, value: unknown): void;
304
+ signals: Record<string, Signal<unknown>>;
305
+ }
284
306
  interface RenderContext {
285
307
  state: StateStore;
286
308
  actions: Record<string, CompiledAction>;
@@ -289,6 +311,10 @@ interface RenderContext {
289
311
  cleanups?: (() => void)[];
290
312
  refs?: Record<string, Element>;
291
313
  inSvg?: boolean;
314
+ localState?: {
315
+ store: LocalStateStore;
316
+ actions: Record<string, CompiledLocalAction>;
317
+ };
292
318
  }
293
319
  declare function render(node: CompiledNode, ctx: RenderContext): Node;
294
320
 
package/dist/index.js CHANGED
@@ -590,38 +590,49 @@ function createEvalContext(ctx) {
590
590
  };
591
591
  }
592
592
  async function executeAction(action, ctx) {
593
+ const extAction = action;
594
+ const isLocal = extAction._isLocalAction && extAction._localStore;
595
+ const localStore = extAction._localStore;
593
596
  for (const step of action.steps) {
594
597
  if (step.do === "set" || step.do === "update" || step.do === "setPath") {
595
- executeStepSync(step, ctx);
598
+ executeStepSync(step, ctx, isLocal ? localStore : void 0);
596
599
  } else if (step.do === "if") {
597
- await executeIfStep(step, ctx);
600
+ await executeIfStep(step, ctx, isLocal ? localStore : void 0);
598
601
  } else {
599
602
  await executeStep(step, ctx);
600
603
  }
601
604
  }
602
605
  }
603
- function executeStepSync(step, ctx) {
606
+ function executeStepSync(step, ctx, localStore) {
604
607
  switch (step.do) {
605
608
  case "set":
606
- executeSetStepSync(step.target, step.value, ctx);
609
+ if (localStore) {
610
+ executeLocalSetStepSync(step.target, step.value, ctx, localStore);
611
+ } else {
612
+ executeSetStepSync(step.target, step.value, ctx);
613
+ }
607
614
  break;
608
615
  case "update":
609
- executeUpdateStepSync(step, ctx);
616
+ if (localStore) {
617
+ executeLocalUpdateStepSync(step, ctx, localStore);
618
+ } else {
619
+ executeUpdateStepSync(step, ctx);
620
+ }
610
621
  break;
611
622
  case "setPath":
612
623
  executeSetPathStepSync(step, ctx);
613
624
  break;
614
625
  }
615
626
  }
616
- async function executeIfStep(step, ctx) {
627
+ async function executeIfStep(step, ctx, localStore) {
617
628
  const evalCtx = createEvalContext(ctx);
618
629
  const condition = evaluate(step.condition, evalCtx);
619
630
  const stepsToExecute = condition ? step.then : step.else || [];
620
631
  for (const nestedStep of stepsToExecute) {
621
632
  if (nestedStep.do === "set" || nestedStep.do === "update" || nestedStep.do === "setPath") {
622
- executeStepSync(nestedStep, ctx);
633
+ executeStepSync(nestedStep, ctx, localStore);
623
634
  } else if (nestedStep.do === "if") {
624
- await executeIfStep(nestedStep, ctx);
635
+ await executeIfStep(nestedStep, ctx, localStore);
625
636
  } else {
626
637
  await executeStep(nestedStep, ctx);
627
638
  }
@@ -718,6 +729,97 @@ function executeUpdateStepSync(step, ctx) {
718
729
  }
719
730
  }
720
731
  }
732
+ function executeLocalSetStepSync(target, value, ctx, localStore) {
733
+ const evalCtx = createEvalContext(ctx);
734
+ const newValue = evaluate(value, evalCtx);
735
+ localStore.set(target, newValue);
736
+ }
737
+ function executeLocalUpdateStepSync(step, ctx, localStore) {
738
+ const { target, operation, value } = step;
739
+ const evalCtx = createEvalContext(ctx);
740
+ const currentValue = localStore.get(target);
741
+ switch (operation) {
742
+ case "toggle": {
743
+ const current = typeof currentValue === "boolean" ? currentValue : false;
744
+ localStore.set(target, !current);
745
+ break;
746
+ }
747
+ case "increment": {
748
+ const evalResult = value ? evaluate(value, evalCtx) : 1;
749
+ const amount = typeof evalResult === "number" ? evalResult : 1;
750
+ const current = typeof currentValue === "number" ? currentValue : 0;
751
+ localStore.set(target, current + amount);
752
+ break;
753
+ }
754
+ case "decrement": {
755
+ const evalResult = value ? evaluate(value, evalCtx) : 1;
756
+ const amount = typeof evalResult === "number" ? evalResult : 1;
757
+ const current = typeof currentValue === "number" ? currentValue : 0;
758
+ localStore.set(target, current - amount);
759
+ break;
760
+ }
761
+ case "push": {
762
+ const item = value ? evaluate(value, evalCtx) : void 0;
763
+ const arr = Array.isArray(currentValue) ? currentValue : [];
764
+ localStore.set(target, [...arr, item]);
765
+ break;
766
+ }
767
+ case "pop": {
768
+ const arr = Array.isArray(currentValue) ? currentValue : [];
769
+ localStore.set(target, arr.slice(0, -1));
770
+ break;
771
+ }
772
+ case "remove": {
773
+ const removeValue = value ? evaluate(value, evalCtx) : void 0;
774
+ const arr = Array.isArray(currentValue) ? currentValue : [];
775
+ if (typeof removeValue === "number") {
776
+ localStore.set(target, arr.filter((_2, i) => i !== removeValue));
777
+ } else {
778
+ localStore.set(target, arr.filter((x2) => x2 !== removeValue));
779
+ }
780
+ break;
781
+ }
782
+ case "merge": {
783
+ const evalResult = value ? evaluate(value, evalCtx) : {};
784
+ const mergeValue = typeof evalResult === "object" && evalResult !== null ? evalResult : {};
785
+ const current = typeof currentValue === "object" && currentValue !== null ? currentValue : {};
786
+ localStore.set(target, { ...current, ...mergeValue });
787
+ break;
788
+ }
789
+ case "replaceAt": {
790
+ const idx = step.index ? evaluate(step.index, evalCtx) : 0;
791
+ const newValue = value ? evaluate(value, evalCtx) : void 0;
792
+ const arr = Array.isArray(currentValue) ? [...currentValue] : [];
793
+ if (typeof idx === "number" && idx >= 0 && idx < arr.length) {
794
+ arr[idx] = newValue;
795
+ }
796
+ localStore.set(target, arr);
797
+ break;
798
+ }
799
+ case "insertAt": {
800
+ const idx = step.index ? evaluate(step.index, evalCtx) : 0;
801
+ const newValue = value ? evaluate(value, evalCtx) : void 0;
802
+ const arr = Array.isArray(currentValue) ? [...currentValue] : [];
803
+ if (typeof idx === "number" && idx >= 0) {
804
+ arr.splice(idx, 0, newValue);
805
+ }
806
+ localStore.set(target, arr);
807
+ break;
808
+ }
809
+ case "splice": {
810
+ const idx = step.index ? evaluate(step.index, evalCtx) : 0;
811
+ const delCount = step.deleteCount ? evaluate(step.deleteCount, evalCtx) : 0;
812
+ const items = value ? evaluate(value, evalCtx) : [];
813
+ const arr = Array.isArray(currentValue) ? [...currentValue] : [];
814
+ if (typeof idx === "number" && typeof delCount === "number") {
815
+ const insertItems = Array.isArray(items) ? items : [];
816
+ arr.splice(idx, delCount, ...insertItems);
817
+ }
818
+ localStore.set(target, arr);
819
+ break;
820
+ }
821
+ }
822
+ }
721
823
  function executeSetPathStepSync(step, ctx) {
722
824
  const evalCtx = createEvalContext(ctx);
723
825
  const pathValue = evaluate(step.path, evalCtx);
@@ -13160,6 +13262,8 @@ function render(node, ctx) {
13160
13262
  return renderMarkdown(node, ctx);
13161
13263
  case "code":
13162
13264
  return renderCode(node, ctx);
13265
+ case "localState":
13266
+ return renderLocalState(node, ctx);
13163
13267
  default:
13164
13268
  throw new Error("Unknown node kind");
13165
13269
  }
@@ -13342,6 +13446,18 @@ function createReactiveLocals(baseLocals, itemKey, itemSignal, indexKey, indexSi
13342
13446
  if (prop === itemKey) return true;
13343
13447
  if (indexKey && prop === indexKey) return true;
13344
13448
  return prop in target;
13449
+ },
13450
+ ownKeys(target) {
13451
+ const keys = Reflect.ownKeys(target);
13452
+ if (!keys.includes(itemKey)) keys.push(itemKey);
13453
+ if (indexKey && !keys.includes(indexKey)) keys.push(indexKey);
13454
+ return keys;
13455
+ },
13456
+ getOwnPropertyDescriptor(target, prop) {
13457
+ if (prop === itemKey || indexKey && prop === indexKey) {
13458
+ return { enumerable: true, configurable: true };
13459
+ }
13460
+ return Reflect.getOwnPropertyDescriptor(target, prop);
13345
13461
  }
13346
13462
  });
13347
13463
  }
@@ -13535,6 +13651,100 @@ function renderCode(node, ctx) {
13535
13651
  ctx.cleanups?.push(cleanup);
13536
13652
  return container;
13537
13653
  }
13654
+ function createLocalStateStore(stateDefs) {
13655
+ const signals = {};
13656
+ for (const [name, def] of Object.entries(stateDefs)) {
13657
+ signals[name] = createSignal(def.initial);
13658
+ }
13659
+ return {
13660
+ get(name) {
13661
+ return signals[name]?.get();
13662
+ },
13663
+ set(name, value) {
13664
+ signals[name]?.set(value);
13665
+ },
13666
+ signals
13667
+ };
13668
+ }
13669
+ function createLocalsWithLocalState(baseLocals, localStore) {
13670
+ return new Proxy(baseLocals, {
13671
+ get(target, prop) {
13672
+ if (prop in localStore.signals) {
13673
+ return localStore.get(prop);
13674
+ }
13675
+ return target[prop];
13676
+ },
13677
+ has(target, prop) {
13678
+ if (prop in localStore.signals) return true;
13679
+ return prop in target;
13680
+ },
13681
+ ownKeys(target) {
13682
+ const keys = Reflect.ownKeys(target);
13683
+ for (const key2 of Object.keys(localStore.signals)) {
13684
+ if (!keys.includes(key2)) keys.push(key2);
13685
+ }
13686
+ return keys;
13687
+ },
13688
+ getOwnPropertyDescriptor(target, prop) {
13689
+ if (prop in localStore.signals) {
13690
+ return { enumerable: true, configurable: true };
13691
+ }
13692
+ return Reflect.getOwnPropertyDescriptor(target, prop);
13693
+ }
13694
+ });
13695
+ }
13696
+ function createStateWithLocalState(globalState, localStore) {
13697
+ return {
13698
+ get(name) {
13699
+ if (name in localStore.signals) {
13700
+ return localStore.get(name);
13701
+ }
13702
+ return globalState.get(name);
13703
+ },
13704
+ set(name, value) {
13705
+ globalState.set(name, value);
13706
+ },
13707
+ setPath(name, path, value) {
13708
+ globalState.setPath(name, path, value);
13709
+ },
13710
+ subscribe(name, fn) {
13711
+ if (name in localStore.signals) {
13712
+ return localStore.signals[name].subscribe(fn);
13713
+ }
13714
+ return globalState.subscribe(name, fn);
13715
+ },
13716
+ getPath(name, path) {
13717
+ return globalState.getPath(name, path);
13718
+ },
13719
+ subscribeToPath(name, path, fn) {
13720
+ return globalState.subscribeToPath(name, path, fn);
13721
+ }
13722
+ };
13723
+ }
13724
+ function renderLocalState(node, ctx) {
13725
+ const localStore = createLocalStateStore(node.state);
13726
+ const mergedLocals = createLocalsWithLocalState(ctx.locals, localStore);
13727
+ const mergedState = createStateWithLocalState(ctx.state, localStore);
13728
+ const mergedActions = { ...ctx.actions };
13729
+ for (const [name, action] of Object.entries(node.actions)) {
13730
+ mergedActions[name] = {
13731
+ ...action,
13732
+ _isLocalAction: true,
13733
+ _localStore: localStore
13734
+ };
13735
+ }
13736
+ const childCtx = {
13737
+ ...ctx,
13738
+ state: mergedState,
13739
+ locals: mergedLocals,
13740
+ actions: mergedActions,
13741
+ localState: {
13742
+ store: localStore,
13743
+ actions: node.actions
13744
+ }
13745
+ };
13746
+ return render(node.child, childCtx);
13747
+ }
13538
13748
 
13539
13749
  // src/app.ts
13540
13750
  function createApp(program, mount) {
@@ -13616,6 +13826,18 @@ function createReactiveLocals2(baseLocals, itemSignal, indexSignal, itemName, in
13616
13826
  if (prop === itemName) return true;
13617
13827
  if (indexName && prop === indexName) return true;
13618
13828
  return prop in target;
13829
+ },
13830
+ ownKeys(target) {
13831
+ const keys = Reflect.ownKeys(target);
13832
+ if (!keys.includes(itemName)) keys.push(itemName);
13833
+ if (indexName && !keys.includes(indexName)) keys.push(indexName);
13834
+ return keys;
13835
+ },
13836
+ getOwnPropertyDescriptor(target, prop) {
13837
+ if (prop === itemName || indexName && prop === indexName) {
13838
+ return { enumerable: true, configurable: true };
13839
+ }
13840
+ return Reflect.getOwnPropertyDescriptor(target, prop);
13619
13841
  }
13620
13842
  });
13621
13843
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/runtime",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
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.1",
22
- "@constela/core": "0.9.1"
21
+ "@constela/compiler": "0.10.0",
22
+ "@constela/core": "0.10.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": "5.0.1"
32
+ "@constela/server": "6.0.0"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=20.0.0"