@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 +10 -0
- package/dist/index.js +270 -1
- package/package.json +4 -4
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
|
-
|
|
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.
|
|
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"
|