@constela/runtime 0.7.0 → 0.8.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,12 @@ 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>;
53
59
  }
54
60
  declare function evaluate(expr: CompiledExpression, ctx: EvaluationContext): unknown;
55
61
 
@@ -60,6 +66,9 @@ declare function evaluate(expr: CompiledExpression, ctx: EvaluationContext): unk
60
66
  * - set: Update state with value
61
67
  * - update: Increment/decrement numbers, push/pop/remove for arrays
62
68
  * - fetch: Make HTTP requests with onSuccess/onError handlers
69
+ * - storage: localStorage/sessionStorage operations
70
+ * - clipboard: Clipboard API operations
71
+ * - navigate: Page navigation
63
72
  */
64
73
 
65
74
  interface ActionContext {
package/dist/index.js CHANGED
@@ -172,12 +172,59 @@ 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
+ }
175
196
  default: {
176
197
  const _exhaustiveCheck = expr;
177
198
  throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustiveCheck)}`);
178
199
  }
179
200
  }
180
201
  }
202
+ function getNestedValue(obj, path) {
203
+ const forbiddenKeys = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
204
+ const parts = path.split(".");
205
+ let value = obj;
206
+ for (const part of parts) {
207
+ if (forbiddenKeys.has(part)) {
208
+ return void 0;
209
+ }
210
+ if (value == null) {
211
+ return void 0;
212
+ }
213
+ if (Array.isArray(value)) {
214
+ const index = Number(part);
215
+ if (Number.isInteger(index) && index >= 0) {
216
+ value = value[index];
217
+ } else {
218
+ value = value[part];
219
+ }
220
+ } else if (typeof value === "object") {
221
+ value = value[part];
222
+ } else {
223
+ return void 0;
224
+ }
225
+ }
226
+ return value;
227
+ }
181
228
  function evaluateBinary(op, left, right, ctx) {
182
229
  if (op === "&&") {
183
230
  const leftVal2 = evaluate(left, ctx);
@@ -243,7 +290,112 @@ function evaluateBinary(op, left, right, ctx) {
243
290
  // src/action/executor.ts
244
291
  async function executeAction(action, ctx) {
245
292
  for (const step of action.steps) {
246
- await executeStep(step, ctx);
293
+ if (step.do === "set" || step.do === "update") {
294
+ executeStepSync(step, ctx);
295
+ } else {
296
+ await executeStep(step, ctx);
297
+ }
298
+ }
299
+ }
300
+ function executeStepSync(step, ctx) {
301
+ switch (step.do) {
302
+ case "set":
303
+ executeSetStepSync(step.target, step.value, ctx);
304
+ break;
305
+ case "update":
306
+ executeUpdateStepSync(step, ctx);
307
+ break;
308
+ }
309
+ }
310
+ function executeSetStepSync(target, value, ctx) {
311
+ const evalCtx = { state: ctx.state, locals: ctx.locals };
312
+ const newValue = evaluate(value, evalCtx);
313
+ ctx.state.set(target, newValue);
314
+ }
315
+ function executeUpdateStepSync(step, ctx) {
316
+ const { target, operation, value } = step;
317
+ const evalCtx = { state: ctx.state, locals: ctx.locals };
318
+ const currentValue = ctx.state.get(target);
319
+ switch (operation) {
320
+ case "increment": {
321
+ const evalResult = value ? evaluate(value, evalCtx) : 1;
322
+ const amount = typeof evalResult === "number" ? evalResult : 1;
323
+ const current = typeof currentValue === "number" ? currentValue : 0;
324
+ ctx.state.set(target, current + amount);
325
+ break;
326
+ }
327
+ case "decrement": {
328
+ const evalResult = value ? evaluate(value, evalCtx) : 1;
329
+ const amount = typeof evalResult === "number" ? evalResult : 1;
330
+ const current = typeof currentValue === "number" ? currentValue : 0;
331
+ ctx.state.set(target, current - amount);
332
+ break;
333
+ }
334
+ case "push": {
335
+ const item = value ? evaluate(value, evalCtx) : void 0;
336
+ const arr = Array.isArray(currentValue) ? currentValue : [];
337
+ ctx.state.set(target, [...arr, item]);
338
+ break;
339
+ }
340
+ case "pop": {
341
+ const arr = Array.isArray(currentValue) ? currentValue : [];
342
+ ctx.state.set(target, arr.slice(0, -1));
343
+ break;
344
+ }
345
+ case "remove": {
346
+ const removeValue = value ? evaluate(value, evalCtx) : void 0;
347
+ const arr = Array.isArray(currentValue) ? currentValue : [];
348
+ if (typeof removeValue === "number") {
349
+ ctx.state.set(target, arr.filter((_, i) => i !== removeValue));
350
+ } else {
351
+ ctx.state.set(target, arr.filter((x) => x !== removeValue));
352
+ }
353
+ break;
354
+ }
355
+ case "toggle": {
356
+ const current = typeof currentValue === "boolean" ? currentValue : false;
357
+ ctx.state.set(target, !current);
358
+ break;
359
+ }
360
+ case "merge": {
361
+ const evalResult = value ? evaluate(value, evalCtx) : {};
362
+ const mergeValue = typeof evalResult === "object" && evalResult !== null ? evalResult : {};
363
+ const current = typeof currentValue === "object" && currentValue !== null ? currentValue : {};
364
+ ctx.state.set(target, { ...current, ...mergeValue });
365
+ break;
366
+ }
367
+ case "replaceAt": {
368
+ const idx = step.index ? evaluate(step.index, evalCtx) : 0;
369
+ const newValue = value ? evaluate(value, evalCtx) : void 0;
370
+ const arr = Array.isArray(currentValue) ? [...currentValue] : [];
371
+ if (typeof idx === "number" && idx >= 0 && idx < arr.length) {
372
+ arr[idx] = newValue;
373
+ }
374
+ ctx.state.set(target, arr);
375
+ break;
376
+ }
377
+ case "insertAt": {
378
+ const idx = step.index ? evaluate(step.index, evalCtx) : 0;
379
+ const newValue = value ? evaluate(value, evalCtx) : void 0;
380
+ const arr = Array.isArray(currentValue) ? [...currentValue] : [];
381
+ if (typeof idx === "number" && idx >= 0) {
382
+ arr.splice(idx, 0, newValue);
383
+ }
384
+ ctx.state.set(target, arr);
385
+ break;
386
+ }
387
+ case "splice": {
388
+ const idx = step.index ? evaluate(step.index, evalCtx) : 0;
389
+ const delCount = step.deleteCount ? evaluate(step.deleteCount, evalCtx) : 0;
390
+ const items = value ? evaluate(value, evalCtx) : [];
391
+ const arr = Array.isArray(currentValue) ? [...currentValue] : [];
392
+ if (typeof idx === "number" && typeof delCount === "number") {
393
+ const insertItems = Array.isArray(items) ? items : [];
394
+ arr.splice(idx, delCount, ...insertItems);
395
+ }
396
+ ctx.state.set(target, arr);
397
+ break;
398
+ }
247
399
  }
248
400
  }
249
401
  async function executeStep(step, ctx) {
@@ -257,6 +409,15 @@ async function executeStep(step, ctx) {
257
409
  case "fetch":
258
410
  await executeFetchStep(step, ctx);
259
411
  break;
412
+ case "storage":
413
+ await executeStorageStep(step, ctx);
414
+ break;
415
+ case "clipboard":
416
+ await executeClipboardStep(step, ctx);
417
+ break;
418
+ case "navigate":
419
+ await executeNavigateStep(step, ctx);
420
+ break;
260
421
  }
261
422
  }
262
423
  async function executeSetStep(target, value, ctx) {
@@ -387,6 +548,90 @@ async function executeFetchStep(step, ctx) {
387
548
  }
388
549
  }
389
550
  }
551
+ async function executeStorageStep(step, ctx) {
552
+ const evalCtx = { state: ctx.state, locals: ctx.locals };
553
+ const key = evaluate(step.key, evalCtx);
554
+ const storage = step.storage === "local" ? localStorage : sessionStorage;
555
+ try {
556
+ switch (step.operation) {
557
+ case "get": {
558
+ const value = storage.getItem(key);
559
+ if (step.result) {
560
+ try {
561
+ ctx.locals[step.result] = value !== null ? JSON.parse(value) : null;
562
+ } catch {
563
+ ctx.locals[step.result] = value;
564
+ }
565
+ }
566
+ break;
567
+ }
568
+ case "set": {
569
+ const setValue = step.value ? evaluate(step.value, evalCtx) : void 0;
570
+ const valueToStore = JSON.stringify(setValue);
571
+ storage.setItem(key, valueToStore);
572
+ break;
573
+ }
574
+ case "remove": {
575
+ storage.removeItem(key);
576
+ break;
577
+ }
578
+ }
579
+ if (step.onSuccess) {
580
+ for (const successStep of step.onSuccess) {
581
+ await executeStep(successStep, ctx);
582
+ }
583
+ }
584
+ } catch (_error) {
585
+ if (step.onError) {
586
+ for (const errorStep of step.onError) {
587
+ await executeStep(errorStep, ctx);
588
+ }
589
+ }
590
+ }
591
+ }
592
+ async function executeClipboardStep(step, ctx) {
593
+ const evalCtx = { state: ctx.state, locals: ctx.locals };
594
+ try {
595
+ switch (step.operation) {
596
+ case "write": {
597
+ const value = step.value ? evaluate(step.value, evalCtx) : "";
598
+ const text = typeof value === "string" ? value : String(value);
599
+ await navigator.clipboard.writeText(text);
600
+ break;
601
+ }
602
+ case "read": {
603
+ const readText = await navigator.clipboard.readText();
604
+ if (step.result) {
605
+ ctx.locals[step.result] = readText;
606
+ }
607
+ break;
608
+ }
609
+ }
610
+ if (step.onSuccess) {
611
+ for (const successStep of step.onSuccess) {
612
+ await executeStep(successStep, ctx);
613
+ }
614
+ }
615
+ } catch (_error) {
616
+ if (step.onError) {
617
+ for (const errorStep of step.onError) {
618
+ await executeStep(errorStep, ctx);
619
+ }
620
+ }
621
+ }
622
+ }
623
+ async function executeNavigateStep(step, ctx) {
624
+ const evalCtx = { state: ctx.state, locals: ctx.locals };
625
+ const url = evaluate(step.url, evalCtx);
626
+ const target = step.target ?? "_self";
627
+ if (target === "_blank") {
628
+ window.open(url, "_blank");
629
+ } else if (step.replace) {
630
+ window.location.replace(url);
631
+ } else {
632
+ window.location.assign(url);
633
+ }
634
+ }
390
635
 
391
636
  // src/renderer/markdown.ts
392
637
  import { marked } from "marked";
@@ -716,6 +961,17 @@ function createApp(program, mount) {
716
961
  locals: {},
717
962
  cleanups
718
963
  };
964
+ const actionCtx = {
965
+ state,
966
+ actions,
967
+ locals: {}
968
+ };
969
+ if (program.lifecycle?.onMount) {
970
+ const onMountAction = actions[program.lifecycle.onMount];
971
+ if (onMountAction) {
972
+ void executeAction(onMountAction, actionCtx);
973
+ }
974
+ }
719
975
  const rootNode = render(program.view, ctx);
720
976
  mount.appendChild(rootNode);
721
977
  let destroyed = false;
@@ -723,6 +979,12 @@ function createApp(program, mount) {
723
979
  destroy() {
724
980
  if (destroyed) return;
725
981
  destroyed = true;
982
+ if (program.lifecycle?.onUnmount) {
983
+ const onUnmountAction = actions[program.lifecycle.onUnmount];
984
+ if (onUnmountAction) {
985
+ void executeAction(onUnmountAction, actionCtx);
986
+ }
987
+ }
726
988
  for (const cleanup of cleanups) {
727
989
  cleanup();
728
990
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/runtime",
3
- "version": "0.7.0",
3
+ "version": "0.8.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.5.0",
22
+ "@constela/core": "0.5.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": "1.0.0"
33
33
  },
34
34
  "engines": {
35
35
  "node": ">=20.0.0"