@constela/runtime 0.7.0 → 0.9.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/dist/index.d.ts CHANGED
@@ -50,6 +50,13 @@ declare function createStateStore(definitions: Record<string, StateDefinition>):
50
50
  interface EvaluationContext {
51
51
  state: StateStore;
52
52
  locals: Record<string, unknown>;
53
+ route?: {
54
+ params: Record<string, string>;
55
+ query: Record<string, string>;
56
+ path: string;
57
+ };
58
+ imports?: Record<string, unknown>;
59
+ refs?: Record<string, Element>;
53
60
  }
54
61
  declare function evaluate(expr: CompiledExpression, ctx: EvaluationContext): unknown;
55
62
 
@@ -60,6 +67,9 @@ declare function evaluate(expr: CompiledExpression, ctx: EvaluationContext): unk
60
67
  * - set: Update state with value
61
68
  * - update: Increment/decrement numbers, push/pop/remove for arrays
62
69
  * - fetch: Make HTTP requests with onSuccess/onError handlers
70
+ * - storage: localStorage/sessionStorage operations
71
+ * - clipboard: Clipboard API operations
72
+ * - navigate: Page navigation
63
73
  */
64
74
 
65
75
  interface ActionContext {
@@ -67,6 +77,8 @@ interface ActionContext {
67
77
  actions: Record<string, CompiledAction>;
68
78
  locals: Record<string, unknown>;
69
79
  eventPayload?: unknown;
80
+ refs?: Record<string, Element>;
81
+ subscriptions?: (() => void)[];
70
82
  }
71
83
  declare function executeAction(action: CompiledAction, ctx: ActionContext): Promise<void>;
72
84
 
package/dist/index.js CHANGED
@@ -172,12 +172,61 @@ function evaluate(expr, ctx) {
172
172
  }
173
173
  return value;
174
174
  }
175
+ case "route": {
176
+ const source = expr.source ?? "param";
177
+ const routeCtx = ctx.route;
178
+ if (!routeCtx) return "";
179
+ switch (source) {
180
+ case "param":
181
+ return routeCtx.params[expr.name] ?? "";
182
+ case "query":
183
+ return routeCtx.query[expr.name] ?? "";
184
+ case "path":
185
+ return routeCtx.path;
186
+ }
187
+ }
188
+ case "import": {
189
+ const importData = ctx.imports?.[expr.name];
190
+ if (importData === void 0) return void 0;
191
+ if (expr.path) {
192
+ return getNestedValue(importData, expr.path);
193
+ }
194
+ return importData;
195
+ }
196
+ case "ref":
197
+ return ctx.refs?.[expr.name] ?? null;
175
198
  default: {
176
199
  const _exhaustiveCheck = expr;
177
200
  throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustiveCheck)}`);
178
201
  }
179
202
  }
180
203
  }
204
+ function getNestedValue(obj, path) {
205
+ const forbiddenKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
206
+ const parts = path.split(".");
207
+ let value = obj;
208
+ for (const part of parts) {
209
+ if (forbiddenKeys.has(part)) {
210
+ return void 0;
211
+ }
212
+ if (value == null) {
213
+ return void 0;
214
+ }
215
+ if (Array.isArray(value)) {
216
+ const index = Number(part);
217
+ if (Number.isInteger(index) && index >= 0) {
218
+ value = value[index];
219
+ } else {
220
+ value = value[part];
221
+ }
222
+ } else if (typeof value === "object") {
223
+ value = value[part];
224
+ } else {
225
+ return void 0;
226
+ }
227
+ }
228
+ return value;
229
+ }
181
230
  function evaluateBinary(op, left, right, ctx) {
182
231
  if (op === "&&") {
183
232
  const leftVal2 = evaluate(left, ctx);
@@ -243,7 +292,112 @@ function evaluateBinary(op, left, right, ctx) {
243
292
  // src/action/executor.ts
244
293
  async function executeAction(action, ctx) {
245
294
  for (const step of action.steps) {
246
- await executeStep(step, ctx);
295
+ if (step.do === "set" || step.do === "update") {
296
+ executeStepSync(step, ctx);
297
+ } else {
298
+ await executeStep(step, ctx);
299
+ }
300
+ }
301
+ }
302
+ function executeStepSync(step, ctx) {
303
+ switch (step.do) {
304
+ case "set":
305
+ executeSetStepSync(step.target, step.value, ctx);
306
+ break;
307
+ case "update":
308
+ executeUpdateStepSync(step, ctx);
309
+ break;
310
+ }
311
+ }
312
+ function executeSetStepSync(target, value, ctx) {
313
+ const evalCtx = { state: ctx.state, locals: ctx.locals };
314
+ const newValue = evaluate(value, evalCtx);
315
+ ctx.state.set(target, newValue);
316
+ }
317
+ function executeUpdateStepSync(step, ctx) {
318
+ const { target, operation, value } = step;
319
+ const evalCtx = { state: ctx.state, locals: ctx.locals };
320
+ const currentValue = ctx.state.get(target);
321
+ switch (operation) {
322
+ case "increment": {
323
+ const evalResult = value ? evaluate(value, evalCtx) : 1;
324
+ const amount = typeof evalResult === "number" ? evalResult : 1;
325
+ const current = typeof currentValue === "number" ? currentValue : 0;
326
+ ctx.state.set(target, current + amount);
327
+ break;
328
+ }
329
+ case "decrement": {
330
+ const evalResult = value ? evaluate(value, evalCtx) : 1;
331
+ const amount = typeof evalResult === "number" ? evalResult : 1;
332
+ const current = typeof currentValue === "number" ? currentValue : 0;
333
+ ctx.state.set(target, current - amount);
334
+ break;
335
+ }
336
+ case "push": {
337
+ const item = value ? evaluate(value, evalCtx) : void 0;
338
+ const arr = Array.isArray(currentValue) ? currentValue : [];
339
+ ctx.state.set(target, [...arr, item]);
340
+ break;
341
+ }
342
+ case "pop": {
343
+ const arr = Array.isArray(currentValue) ? currentValue : [];
344
+ ctx.state.set(target, arr.slice(0, -1));
345
+ break;
346
+ }
347
+ case "remove": {
348
+ const removeValue = value ? evaluate(value, evalCtx) : void 0;
349
+ const arr = Array.isArray(currentValue) ? currentValue : [];
350
+ if (typeof removeValue === "number") {
351
+ ctx.state.set(target, arr.filter((_, i) => i !== removeValue));
352
+ } else {
353
+ ctx.state.set(target, arr.filter((x) => x !== removeValue));
354
+ }
355
+ break;
356
+ }
357
+ case "toggle": {
358
+ const current = typeof currentValue === "boolean" ? currentValue : false;
359
+ ctx.state.set(target, !current);
360
+ break;
361
+ }
362
+ case "merge": {
363
+ const evalResult = value ? evaluate(value, evalCtx) : {};
364
+ const mergeValue = typeof evalResult === "object" && evalResult !== null ? evalResult : {};
365
+ const current = typeof currentValue === "object" && currentValue !== null ? currentValue : {};
366
+ ctx.state.set(target, { ...current, ...mergeValue });
367
+ break;
368
+ }
369
+ case "replaceAt": {
370
+ const idx = step.index ? evaluate(step.index, evalCtx) : 0;
371
+ const newValue = value ? evaluate(value, evalCtx) : void 0;
372
+ const arr = Array.isArray(currentValue) ? [...currentValue] : [];
373
+ if (typeof idx === "number" && idx >= 0 && idx < arr.length) {
374
+ arr[idx] = newValue;
375
+ }
376
+ ctx.state.set(target, arr);
377
+ break;
378
+ }
379
+ case "insertAt": {
380
+ const idx = step.index ? evaluate(step.index, evalCtx) : 0;
381
+ const newValue = value ? evaluate(value, evalCtx) : void 0;
382
+ const arr = Array.isArray(currentValue) ? [...currentValue] : [];
383
+ if (typeof idx === "number" && idx >= 0) {
384
+ arr.splice(idx, 0, newValue);
385
+ }
386
+ ctx.state.set(target, arr);
387
+ break;
388
+ }
389
+ case "splice": {
390
+ const idx = step.index ? evaluate(step.index, evalCtx) : 0;
391
+ const delCount = step.deleteCount ? evaluate(step.deleteCount, evalCtx) : 0;
392
+ const items = value ? evaluate(value, evalCtx) : [];
393
+ const arr = Array.isArray(currentValue) ? [...currentValue] : [];
394
+ if (typeof idx === "number" && typeof delCount === "number") {
395
+ const insertItems = Array.isArray(items) ? items : [];
396
+ arr.splice(idx, delCount, ...insertItems);
397
+ }
398
+ ctx.state.set(target, arr);
399
+ break;
400
+ }
247
401
  }
248
402
  }
249
403
  async function executeStep(step, ctx) {
@@ -257,6 +411,27 @@ async function executeStep(step, ctx) {
257
411
  case "fetch":
258
412
  await executeFetchStep(step, ctx);
259
413
  break;
414
+ case "storage":
415
+ await executeStorageStep(step, ctx);
416
+ break;
417
+ case "clipboard":
418
+ await executeClipboardStep(step, ctx);
419
+ break;
420
+ case "navigate":
421
+ await executeNavigateStep(step, ctx);
422
+ break;
423
+ case "import":
424
+ await executeImportStep(step, ctx);
425
+ break;
426
+ case "call":
427
+ await executeCallStep(step, ctx);
428
+ break;
429
+ case "subscribe":
430
+ await executeSubscribeStep(step, ctx);
431
+ break;
432
+ case "dispose":
433
+ await executeDisposeStep(step, ctx);
434
+ break;
260
435
  }
261
436
  }
262
437
  async function executeSetStep(target, value, ctx) {
@@ -373,13 +548,66 @@ async function executeFetchStep(step, ctx) {
373
548
  }
374
549
  }
375
550
  } else {
551
+ ctx.locals["error"] = {
552
+ message: `HTTP error: ${response.status} ${response.statusText}`,
553
+ name: "HTTPError"
554
+ };
376
555
  if (step.onError) {
377
556
  for (const errorStep of step.onError) {
378
557
  await executeStep(errorStep, ctx);
379
558
  }
380
559
  }
381
560
  }
382
- } catch (_error) {
561
+ } catch (err) {
562
+ ctx.locals["error"] = {
563
+ message: err instanceof Error ? err.message : String(err),
564
+ name: err instanceof Error ? err.name : "Error"
565
+ };
566
+ if (step.onError) {
567
+ for (const errorStep of step.onError) {
568
+ await executeStep(errorStep, ctx);
569
+ }
570
+ }
571
+ }
572
+ }
573
+ async function executeStorageStep(step, ctx) {
574
+ const evalCtx = { state: ctx.state, locals: ctx.locals };
575
+ const key = evaluate(step.key, evalCtx);
576
+ const storage = step.storage === "local" ? localStorage : sessionStorage;
577
+ try {
578
+ switch (step.operation) {
579
+ case "get": {
580
+ const value = storage.getItem(key);
581
+ if (step.result) {
582
+ try {
583
+ ctx.locals[step.result] = value !== null ? JSON.parse(value) : null;
584
+ } catch {
585
+ ctx.locals[step.result] = value;
586
+ }
587
+ }
588
+ break;
589
+ }
590
+ case "set": {
591
+ const setValue = step.value ? evaluate(step.value, evalCtx) : void 0;
592
+ const valueToStore = JSON.stringify(setValue);
593
+ storage.setItem(key, valueToStore);
594
+ break;
595
+ }
596
+ case "remove": {
597
+ storage.removeItem(key);
598
+ break;
599
+ }
600
+ }
601
+ if (step.onSuccess) {
602
+ for (const successStep of step.onSuccess) {
603
+ await executeStep(successStep, ctx);
604
+ }
605
+ }
606
+ } catch (err) {
607
+ ctx.locals["error"] = {
608
+ message: err instanceof Error ? err.message : String(err),
609
+ name: err instanceof Error ? err.name : "Error"
610
+ };
383
611
  if (step.onError) {
384
612
  for (const errorStep of step.onError) {
385
613
  await executeStep(errorStep, ctx);
@@ -387,6 +615,145 @@ async function executeFetchStep(step, ctx) {
387
615
  }
388
616
  }
389
617
  }
618
+ async function executeClipboardStep(step, ctx) {
619
+ const evalCtx = { state: ctx.state, locals: ctx.locals };
620
+ try {
621
+ switch (step.operation) {
622
+ case "write": {
623
+ const value = step.value ? evaluate(step.value, evalCtx) : "";
624
+ const text = typeof value === "string" ? value : String(value);
625
+ await navigator.clipboard.writeText(text);
626
+ break;
627
+ }
628
+ case "read": {
629
+ const readText = await navigator.clipboard.readText();
630
+ if (step.result) {
631
+ ctx.locals[step.result] = readText;
632
+ }
633
+ break;
634
+ }
635
+ }
636
+ if (step.onSuccess) {
637
+ for (const successStep of step.onSuccess) {
638
+ await executeStep(successStep, ctx);
639
+ }
640
+ }
641
+ } catch (err) {
642
+ ctx.locals["error"] = {
643
+ message: err instanceof Error ? err.message : String(err),
644
+ name: err instanceof Error ? err.name : "Error"
645
+ };
646
+ if (step.onError) {
647
+ for (const errorStep of step.onError) {
648
+ await executeStep(errorStep, ctx);
649
+ }
650
+ }
651
+ }
652
+ }
653
+ async function executeNavigateStep(step, ctx) {
654
+ const evalCtx = { state: ctx.state, locals: ctx.locals };
655
+ const url = evaluate(step.url, evalCtx);
656
+ const target = step.target ?? "_self";
657
+ if (target === "_blank") {
658
+ window.open(url, "_blank");
659
+ } else if (step.replace) {
660
+ window.location.replace(url);
661
+ } else {
662
+ window.location.assign(url);
663
+ }
664
+ }
665
+ async function executeImportStep(step, ctx) {
666
+ try {
667
+ const module = await import(
668
+ /* @vite-ignore */
669
+ step.module
670
+ );
671
+ ctx.locals[step.result] = module;
672
+ if (step.onSuccess) {
673
+ for (const successStep of step.onSuccess) {
674
+ await executeStep(successStep, ctx);
675
+ }
676
+ }
677
+ } catch (err) {
678
+ ctx.locals["error"] = {
679
+ message: err instanceof Error ? err.message : String(err),
680
+ name: err instanceof Error ? err.name : "Error"
681
+ };
682
+ if (step.onError) {
683
+ for (const errorStep of step.onError) {
684
+ await executeStep(errorStep, ctx);
685
+ }
686
+ }
687
+ }
688
+ }
689
+ async function executeCallStep(step, ctx) {
690
+ const evalCtx = { state: ctx.state, locals: ctx.locals, ...ctx.refs && { refs: ctx.refs } };
691
+ try {
692
+ const target = evaluate(step.target, evalCtx);
693
+ const args = step.args?.map((arg) => evaluate(arg, evalCtx)) ?? [];
694
+ if (typeof target === "function") {
695
+ const result = await target(...args);
696
+ if (step.result) {
697
+ ctx.locals[step.result] = result;
698
+ }
699
+ } else {
700
+ throw new Error(`Target is not callable: received ${typeof target}`);
701
+ }
702
+ if (step.onSuccess) {
703
+ for (const successStep of step.onSuccess) {
704
+ await executeStep(successStep, ctx);
705
+ }
706
+ }
707
+ } catch (err) {
708
+ ctx.locals["error"] = {
709
+ message: err instanceof Error ? err.message : String(err),
710
+ name: err instanceof Error ? err.name : "Error"
711
+ };
712
+ if (step.onError) {
713
+ for (const errorStep of step.onError) {
714
+ await executeStep(errorStep, ctx);
715
+ }
716
+ }
717
+ }
718
+ }
719
+ async function executeSubscribeStep(step, ctx) {
720
+ const evalCtx = { state: ctx.state, locals: ctx.locals, ...ctx.refs && { refs: ctx.refs } };
721
+ const target = evaluate(step.target, evalCtx);
722
+ if (target && typeof target === "object" && step.event in target) {
723
+ const eventMethod = target[step.event];
724
+ if (typeof eventMethod === "function") {
725
+ const disposable = eventMethod.call(target, async (eventData) => {
726
+ const action = ctx.actions[step.action];
727
+ if (action) {
728
+ const subscriptionCtx = {
729
+ ...ctx,
730
+ locals: { ...ctx.locals, event: eventData }
731
+ };
732
+ await executeAction(action, subscriptionCtx);
733
+ }
734
+ });
735
+ if (ctx.subscriptions) {
736
+ if (disposable && typeof disposable === "object" && "dispose" in disposable && typeof disposable.dispose === "function") {
737
+ ctx.subscriptions.push(() => disposable.dispose());
738
+ } else if (typeof disposable === "function") {
739
+ ctx.subscriptions.push(disposable);
740
+ }
741
+ }
742
+ }
743
+ }
744
+ }
745
+ async function executeDisposeStep(step, ctx) {
746
+ const evalCtx = { state: ctx.state, locals: ctx.locals, ...ctx.refs && { refs: ctx.refs } };
747
+ const target = evaluate(step.target, evalCtx);
748
+ if (target && typeof target === "object") {
749
+ const obj = target;
750
+ if (typeof obj["dispose"] === "function") {
751
+ obj["dispose"]();
752
+ } else if (typeof obj["destroy"] === "function") {
753
+ obj["destroy"]();
754
+ }
755
+ }
756
+ }
390
757
 
391
758
  // src/renderer/markdown.ts
392
759
  import { marked } from "marked";
@@ -716,6 +1083,17 @@ function createApp(program, mount) {
716
1083
  locals: {},
717
1084
  cleanups
718
1085
  };
1086
+ const actionCtx = {
1087
+ state,
1088
+ actions,
1089
+ locals: {}
1090
+ };
1091
+ if (program.lifecycle?.onMount) {
1092
+ const onMountAction = actions[program.lifecycle.onMount];
1093
+ if (onMountAction) {
1094
+ void executeAction(onMountAction, actionCtx);
1095
+ }
1096
+ }
719
1097
  const rootNode = render(program.view, ctx);
720
1098
  mount.appendChild(rootNode);
721
1099
  let destroyed = false;
@@ -723,6 +1101,12 @@ function createApp(program, mount) {
723
1101
  destroy() {
724
1102
  if (destroyed) return;
725
1103
  destroyed = true;
1104
+ if (program.lifecycle?.onUnmount) {
1105
+ const onUnmountAction = actions[program.lifecycle.onUnmount];
1106
+ if (onUnmountAction) {
1107
+ void executeAction(onUnmountAction, actionCtx);
1108
+ }
1109
+ }
726
1110
  for (const cleanup of cleanups) {
727
1111
  cleanup();
728
1112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/runtime",
3
- "version": "0.7.0",
3
+ "version": "0.9.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.4.0",
22
- "@constela/core": "0.4.0"
21
+ "@constela/compiler": "0.6.0",
22
+ "@constela/core": "0.6.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": "0.1.2"
32
+ "@constela/server": "2.0.0"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=20.0.0"