@constela/runtime 0.6.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
@@ -26,6 +26,7 @@ declare function createEffect(fn: EffectFn): () => void;
26
26
  interface StateStore {
27
27
  get(name: string): unknown;
28
28
  set(name: string, value: unknown): void;
29
+ subscribe(name: string, fn: (value: unknown) => void): () => void;
29
30
  }
30
31
  interface StateDefinition {
31
32
  type: string;
@@ -49,6 +50,12 @@ declare function createStateStore(definitions: Record<string, StateDefinition>):
49
50
  interface EvaluationContext {
50
51
  state: StateStore;
51
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>;
52
59
  }
53
60
  declare function evaluate(expr: CompiledExpression, ctx: EvaluationContext): unknown;
54
61
 
@@ -59,6 +66,9 @@ declare function evaluate(expr: CompiledExpression, ctx: EvaluationContext): unk
59
66
  * - set: Update state with value
60
67
  * - update: Increment/decrement numbers, push/pop/remove for arrays
61
68
  * - fetch: Make HTTP requests with onSuccess/onError handlers
69
+ * - storage: localStorage/sessionStorage operations
70
+ * - clipboard: Clipboard API operations
71
+ * - navigate: Page navigation
62
72
  */
63
73
 
64
74
  interface ActionContext {
package/dist/index.js CHANGED
@@ -111,6 +111,13 @@ function createStateStore(definitions) {
111
111
  throw new Error(`State field "${name}" does not exist`);
112
112
  }
113
113
  signal.set(value);
114
+ },
115
+ subscribe(name, fn) {
116
+ const signal = signals.get(name);
117
+ if (!signal) {
118
+ throw new Error(`State field "${name}" does not exist`);
119
+ }
120
+ return signal.subscribe(fn);
114
121
  }
115
122
  };
116
123
  }
@@ -165,12 +172,59 @@ function evaluate(expr, ctx) {
165
172
  }
166
173
  return value;
167
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
+ }
168
196
  default: {
169
197
  const _exhaustiveCheck = expr;
170
198
  throw new Error(`Unknown expression type: ${JSON.stringify(_exhaustiveCheck)}`);
171
199
  }
172
200
  }
173
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
+ }
174
228
  function evaluateBinary(op, left, right, ctx) {
175
229
  if (op === "&&") {
176
230
  const leftVal2 = evaluate(left, ctx);
@@ -236,7 +290,112 @@ function evaluateBinary(op, left, right, ctx) {
236
290
  // src/action/executor.ts
237
291
  async function executeAction(action, ctx) {
238
292
  for (const step of action.steps) {
239
- 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
+ }
240
399
  }
241
400
  }
242
401
  async function executeStep(step, ctx) {
@@ -250,6 +409,15 @@ async function executeStep(step, ctx) {
250
409
  case "fetch":
251
410
  await executeFetchStep(step, ctx);
252
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;
253
421
  }
254
422
  }
255
423
  async function executeSetStep(target, value, ctx) {
@@ -380,6 +548,90 @@ async function executeFetchStep(step, ctx) {
380
548
  }
381
549
  }
382
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
+ }
383
635
 
384
636
  // src/renderer/markdown.ts
385
637
  import { marked } from "marked";
@@ -709,6 +961,17 @@ function createApp(program, mount) {
709
961
  locals: {},
710
962
  cleanups
711
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
+ }
712
975
  const rootNode = render(program.view, ctx);
713
976
  mount.appendChild(rootNode);
714
977
  let destroyed = false;
@@ -716,6 +979,12 @@ function createApp(program, mount) {
716
979
  destroy() {
717
980
  if (destroyed) return;
718
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
+ }
719
988
  for (const cleanup of cleanups) {
720
989
  cleanup();
721
990
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constela/runtime",
3
- "version": "0.6.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"