@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 +9 -0
- package/dist/index.js +263 -1
- package/package.json +4 -4
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
|
-
|
|
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.
|
|
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.
|
|
22
|
-
"@constela/core": "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.
|
|
32
|
+
"@constela/server": "1.0.0"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
35
35
|
"node": ">=20.0.0"
|