@constela/runtime 0.12.1 → 0.13.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/README.md +48 -0
- package/dist/index.d.ts +28 -2
- package/dist/index.js +230 -8
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -65,6 +65,54 @@ Dynamic path with variables:
|
|
|
65
65
|
}
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
### String Concatenation (concat)
|
|
69
|
+
|
|
70
|
+
Build dynamic strings from multiple expressions:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"expr": "concat",
|
|
75
|
+
"items": [
|
|
76
|
+
{ "expr": "lit", "value": "/users/" },
|
|
77
|
+
{ "expr": "var", "name": "userId" },
|
|
78
|
+
{ "expr": "lit", "value": "/profile" }
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Useful for:
|
|
84
|
+
- Dynamic URLs: `/api/posts/{id}`
|
|
85
|
+
- CSS class names: `btn btn-{variant}`
|
|
86
|
+
- Formatted messages: `Hello, {name}!`
|
|
87
|
+
|
|
88
|
+
### Object Payloads for Event Handlers
|
|
89
|
+
|
|
90
|
+
Pass multiple values to actions with object-shaped payloads:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"kind": "element",
|
|
95
|
+
"tag": "button",
|
|
96
|
+
"props": {
|
|
97
|
+
"onClick": {
|
|
98
|
+
"event": "click",
|
|
99
|
+
"action": "toggleLike",
|
|
100
|
+
"payload": {
|
|
101
|
+
"index": { "expr": "var", "name": "index" },
|
|
102
|
+
"postId": { "expr": "var", "name": "post", "path": "id" },
|
|
103
|
+
"currentLiked": { "expr": "var", "name": "post", "path": "liked" }
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Each expression field in the payload is evaluated when the event fires. The action receives the evaluated object:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{ "index": 5, "postId": "abc123", "currentLiked": true }
|
|
114
|
+
```
|
|
115
|
+
|
|
68
116
|
### Key-based List Diffing
|
|
69
117
|
|
|
70
118
|
Efficient list updates - only changed items re-render:
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CompiledExpression, CompiledAction, CompiledNode, CompiledProgram } from '@constela/compiler';
|
|
1
|
+
import { CompiledExpression, CompiledAction, CompiledNode, CompiledLocalAction, CompiledProgram } from '@constela/compiler';
|
|
2
2
|
|
|
3
3
|
interface Signal<T> {
|
|
4
4
|
get(): T;
|
|
@@ -254,6 +254,20 @@ declare function createConnectionManager(): ConnectionManager;
|
|
|
254
254
|
* - navigate: Page navigation
|
|
255
255
|
*/
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Local state store interface for component-level state
|
|
259
|
+
*/
|
|
260
|
+
interface LocalStateStore$1 {
|
|
261
|
+
get(name: string): unknown;
|
|
262
|
+
set(name: string, value: unknown): void;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Extended action type with local action metadata
|
|
266
|
+
*/
|
|
267
|
+
interface ExtendedAction extends CompiledAction {
|
|
268
|
+
_isLocalAction?: boolean;
|
|
269
|
+
_localStore?: LocalStateStore$1;
|
|
270
|
+
}
|
|
257
271
|
interface ActionContext {
|
|
258
272
|
state: StateStore;
|
|
259
273
|
actions: Record<string, CompiledAction>;
|
|
@@ -269,7 +283,7 @@ interface ActionContext {
|
|
|
269
283
|
imports?: Record<string, unknown>;
|
|
270
284
|
connections?: ConnectionManager;
|
|
271
285
|
}
|
|
272
|
-
declare function executeAction(action: CompiledAction, ctx: ActionContext): Promise<void>;
|
|
286
|
+
declare function executeAction(action: CompiledAction | ExtendedAction, ctx: ActionContext): Promise<void>;
|
|
273
287
|
|
|
274
288
|
/**
|
|
275
289
|
* Renderer - DOM rendering for compiled view nodes
|
|
@@ -281,6 +295,14 @@ declare function executeAction(action: CompiledAction, ctx: ActionContext): Prom
|
|
|
281
295
|
* - each: List rendering with reactive updates
|
|
282
296
|
*/
|
|
283
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Local state store interface for component-level state
|
|
300
|
+
*/
|
|
301
|
+
interface LocalStateStore {
|
|
302
|
+
get(name: string): unknown;
|
|
303
|
+
set(name: string, value: unknown): void;
|
|
304
|
+
signals: Record<string, Signal<unknown>>;
|
|
305
|
+
}
|
|
284
306
|
interface RenderContext {
|
|
285
307
|
state: StateStore;
|
|
286
308
|
actions: Record<string, CompiledAction>;
|
|
@@ -289,6 +311,10 @@ interface RenderContext {
|
|
|
289
311
|
cleanups?: (() => void)[];
|
|
290
312
|
refs?: Record<string, Element>;
|
|
291
313
|
inSvg?: boolean;
|
|
314
|
+
localState?: {
|
|
315
|
+
store: LocalStateStore;
|
|
316
|
+
actions: Record<string, CompiledLocalAction>;
|
|
317
|
+
};
|
|
292
318
|
}
|
|
293
319
|
declare function render(node: CompiledNode, ctx: RenderContext): Node;
|
|
294
320
|
|
package/dist/index.js
CHANGED
|
@@ -590,38 +590,49 @@ function createEvalContext(ctx) {
|
|
|
590
590
|
};
|
|
591
591
|
}
|
|
592
592
|
async function executeAction(action, ctx) {
|
|
593
|
+
const extAction = action;
|
|
594
|
+
const isLocal = extAction._isLocalAction && extAction._localStore;
|
|
595
|
+
const localStore = extAction._localStore;
|
|
593
596
|
for (const step of action.steps) {
|
|
594
597
|
if (step.do === "set" || step.do === "update" || step.do === "setPath") {
|
|
595
|
-
executeStepSync(step, ctx);
|
|
598
|
+
executeStepSync(step, ctx, isLocal ? localStore : void 0);
|
|
596
599
|
} else if (step.do === "if") {
|
|
597
|
-
await executeIfStep(step, ctx);
|
|
600
|
+
await executeIfStep(step, ctx, isLocal ? localStore : void 0);
|
|
598
601
|
} else {
|
|
599
602
|
await executeStep(step, ctx);
|
|
600
603
|
}
|
|
601
604
|
}
|
|
602
605
|
}
|
|
603
|
-
function executeStepSync(step, ctx) {
|
|
606
|
+
function executeStepSync(step, ctx, localStore) {
|
|
604
607
|
switch (step.do) {
|
|
605
608
|
case "set":
|
|
606
|
-
|
|
609
|
+
if (localStore) {
|
|
610
|
+
executeLocalSetStepSync(step.target, step.value, ctx, localStore);
|
|
611
|
+
} else {
|
|
612
|
+
executeSetStepSync(step.target, step.value, ctx);
|
|
613
|
+
}
|
|
607
614
|
break;
|
|
608
615
|
case "update":
|
|
609
|
-
|
|
616
|
+
if (localStore) {
|
|
617
|
+
executeLocalUpdateStepSync(step, ctx, localStore);
|
|
618
|
+
} else {
|
|
619
|
+
executeUpdateStepSync(step, ctx);
|
|
620
|
+
}
|
|
610
621
|
break;
|
|
611
622
|
case "setPath":
|
|
612
623
|
executeSetPathStepSync(step, ctx);
|
|
613
624
|
break;
|
|
614
625
|
}
|
|
615
626
|
}
|
|
616
|
-
async function executeIfStep(step, ctx) {
|
|
627
|
+
async function executeIfStep(step, ctx, localStore) {
|
|
617
628
|
const evalCtx = createEvalContext(ctx);
|
|
618
629
|
const condition = evaluate(step.condition, evalCtx);
|
|
619
630
|
const stepsToExecute = condition ? step.then : step.else || [];
|
|
620
631
|
for (const nestedStep of stepsToExecute) {
|
|
621
632
|
if (nestedStep.do === "set" || nestedStep.do === "update" || nestedStep.do === "setPath") {
|
|
622
|
-
executeStepSync(nestedStep, ctx);
|
|
633
|
+
executeStepSync(nestedStep, ctx, localStore);
|
|
623
634
|
} else if (nestedStep.do === "if") {
|
|
624
|
-
await executeIfStep(nestedStep, ctx);
|
|
635
|
+
await executeIfStep(nestedStep, ctx, localStore);
|
|
625
636
|
} else {
|
|
626
637
|
await executeStep(nestedStep, ctx);
|
|
627
638
|
}
|
|
@@ -718,6 +729,97 @@ function executeUpdateStepSync(step, ctx) {
|
|
|
718
729
|
}
|
|
719
730
|
}
|
|
720
731
|
}
|
|
732
|
+
function executeLocalSetStepSync(target, value, ctx, localStore) {
|
|
733
|
+
const evalCtx = createEvalContext(ctx);
|
|
734
|
+
const newValue = evaluate(value, evalCtx);
|
|
735
|
+
localStore.set(target, newValue);
|
|
736
|
+
}
|
|
737
|
+
function executeLocalUpdateStepSync(step, ctx, localStore) {
|
|
738
|
+
const { target, operation, value } = step;
|
|
739
|
+
const evalCtx = createEvalContext(ctx);
|
|
740
|
+
const currentValue = localStore.get(target);
|
|
741
|
+
switch (operation) {
|
|
742
|
+
case "toggle": {
|
|
743
|
+
const current = typeof currentValue === "boolean" ? currentValue : false;
|
|
744
|
+
localStore.set(target, !current);
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
case "increment": {
|
|
748
|
+
const evalResult = value ? evaluate(value, evalCtx) : 1;
|
|
749
|
+
const amount = typeof evalResult === "number" ? evalResult : 1;
|
|
750
|
+
const current = typeof currentValue === "number" ? currentValue : 0;
|
|
751
|
+
localStore.set(target, current + amount);
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
case "decrement": {
|
|
755
|
+
const evalResult = value ? evaluate(value, evalCtx) : 1;
|
|
756
|
+
const amount = typeof evalResult === "number" ? evalResult : 1;
|
|
757
|
+
const current = typeof currentValue === "number" ? currentValue : 0;
|
|
758
|
+
localStore.set(target, current - amount);
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
case "push": {
|
|
762
|
+
const item = value ? evaluate(value, evalCtx) : void 0;
|
|
763
|
+
const arr = Array.isArray(currentValue) ? currentValue : [];
|
|
764
|
+
localStore.set(target, [...arr, item]);
|
|
765
|
+
break;
|
|
766
|
+
}
|
|
767
|
+
case "pop": {
|
|
768
|
+
const arr = Array.isArray(currentValue) ? currentValue : [];
|
|
769
|
+
localStore.set(target, arr.slice(0, -1));
|
|
770
|
+
break;
|
|
771
|
+
}
|
|
772
|
+
case "remove": {
|
|
773
|
+
const removeValue = value ? evaluate(value, evalCtx) : void 0;
|
|
774
|
+
const arr = Array.isArray(currentValue) ? currentValue : [];
|
|
775
|
+
if (typeof removeValue === "number") {
|
|
776
|
+
localStore.set(target, arr.filter((_2, i) => i !== removeValue));
|
|
777
|
+
} else {
|
|
778
|
+
localStore.set(target, arr.filter((x2) => x2 !== removeValue));
|
|
779
|
+
}
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
case "merge": {
|
|
783
|
+
const evalResult = value ? evaluate(value, evalCtx) : {};
|
|
784
|
+
const mergeValue = typeof evalResult === "object" && evalResult !== null ? evalResult : {};
|
|
785
|
+
const current = typeof currentValue === "object" && currentValue !== null ? currentValue : {};
|
|
786
|
+
localStore.set(target, { ...current, ...mergeValue });
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
case "replaceAt": {
|
|
790
|
+
const idx = step.index ? evaluate(step.index, evalCtx) : 0;
|
|
791
|
+
const newValue = value ? evaluate(value, evalCtx) : void 0;
|
|
792
|
+
const arr = Array.isArray(currentValue) ? [...currentValue] : [];
|
|
793
|
+
if (typeof idx === "number" && idx >= 0 && idx < arr.length) {
|
|
794
|
+
arr[idx] = newValue;
|
|
795
|
+
}
|
|
796
|
+
localStore.set(target, arr);
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
case "insertAt": {
|
|
800
|
+
const idx = step.index ? evaluate(step.index, evalCtx) : 0;
|
|
801
|
+
const newValue = value ? evaluate(value, evalCtx) : void 0;
|
|
802
|
+
const arr = Array.isArray(currentValue) ? [...currentValue] : [];
|
|
803
|
+
if (typeof idx === "number" && idx >= 0) {
|
|
804
|
+
arr.splice(idx, 0, newValue);
|
|
805
|
+
}
|
|
806
|
+
localStore.set(target, arr);
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
case "splice": {
|
|
810
|
+
const idx = step.index ? evaluate(step.index, evalCtx) : 0;
|
|
811
|
+
const delCount = step.deleteCount ? evaluate(step.deleteCount, evalCtx) : 0;
|
|
812
|
+
const items = value ? evaluate(value, evalCtx) : [];
|
|
813
|
+
const arr = Array.isArray(currentValue) ? [...currentValue] : [];
|
|
814
|
+
if (typeof idx === "number" && typeof delCount === "number") {
|
|
815
|
+
const insertItems = Array.isArray(items) ? items : [];
|
|
816
|
+
arr.splice(idx, delCount, ...insertItems);
|
|
817
|
+
}
|
|
818
|
+
localStore.set(target, arr);
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
721
823
|
function executeSetPathStepSync(step, ctx) {
|
|
722
824
|
const evalCtx = createEvalContext(ctx);
|
|
723
825
|
const pathValue = evaluate(step.path, evalCtx);
|
|
@@ -13160,6 +13262,8 @@ function render(node, ctx) {
|
|
|
13160
13262
|
return renderMarkdown(node, ctx);
|
|
13161
13263
|
case "code":
|
|
13162
13264
|
return renderCode(node, ctx);
|
|
13265
|
+
case "localState":
|
|
13266
|
+
return renderLocalState(node, ctx);
|
|
13163
13267
|
default:
|
|
13164
13268
|
throw new Error("Unknown node kind");
|
|
13165
13269
|
}
|
|
@@ -13342,6 +13446,18 @@ function createReactiveLocals(baseLocals, itemKey, itemSignal, indexKey, indexSi
|
|
|
13342
13446
|
if (prop === itemKey) return true;
|
|
13343
13447
|
if (indexKey && prop === indexKey) return true;
|
|
13344
13448
|
return prop in target;
|
|
13449
|
+
},
|
|
13450
|
+
ownKeys(target) {
|
|
13451
|
+
const keys = Reflect.ownKeys(target);
|
|
13452
|
+
if (!keys.includes(itemKey)) keys.push(itemKey);
|
|
13453
|
+
if (indexKey && !keys.includes(indexKey)) keys.push(indexKey);
|
|
13454
|
+
return keys;
|
|
13455
|
+
},
|
|
13456
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
13457
|
+
if (prop === itemKey || indexKey && prop === indexKey) {
|
|
13458
|
+
return { enumerable: true, configurable: true };
|
|
13459
|
+
}
|
|
13460
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
13345
13461
|
}
|
|
13346
13462
|
});
|
|
13347
13463
|
}
|
|
@@ -13535,6 +13651,100 @@ function renderCode(node, ctx) {
|
|
|
13535
13651
|
ctx.cleanups?.push(cleanup);
|
|
13536
13652
|
return container;
|
|
13537
13653
|
}
|
|
13654
|
+
function createLocalStateStore(stateDefs) {
|
|
13655
|
+
const signals = {};
|
|
13656
|
+
for (const [name, def] of Object.entries(stateDefs)) {
|
|
13657
|
+
signals[name] = createSignal(def.initial);
|
|
13658
|
+
}
|
|
13659
|
+
return {
|
|
13660
|
+
get(name) {
|
|
13661
|
+
return signals[name]?.get();
|
|
13662
|
+
},
|
|
13663
|
+
set(name, value) {
|
|
13664
|
+
signals[name]?.set(value);
|
|
13665
|
+
},
|
|
13666
|
+
signals
|
|
13667
|
+
};
|
|
13668
|
+
}
|
|
13669
|
+
function createLocalsWithLocalState(baseLocals, localStore) {
|
|
13670
|
+
return new Proxy(baseLocals, {
|
|
13671
|
+
get(target, prop) {
|
|
13672
|
+
if (prop in localStore.signals) {
|
|
13673
|
+
return localStore.get(prop);
|
|
13674
|
+
}
|
|
13675
|
+
return target[prop];
|
|
13676
|
+
},
|
|
13677
|
+
has(target, prop) {
|
|
13678
|
+
if (prop in localStore.signals) return true;
|
|
13679
|
+
return prop in target;
|
|
13680
|
+
},
|
|
13681
|
+
ownKeys(target) {
|
|
13682
|
+
const keys = Reflect.ownKeys(target);
|
|
13683
|
+
for (const key2 of Object.keys(localStore.signals)) {
|
|
13684
|
+
if (!keys.includes(key2)) keys.push(key2);
|
|
13685
|
+
}
|
|
13686
|
+
return keys;
|
|
13687
|
+
},
|
|
13688
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
13689
|
+
if (prop in localStore.signals) {
|
|
13690
|
+
return { enumerable: true, configurable: true };
|
|
13691
|
+
}
|
|
13692
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
13693
|
+
}
|
|
13694
|
+
});
|
|
13695
|
+
}
|
|
13696
|
+
function createStateWithLocalState(globalState, localStore) {
|
|
13697
|
+
return {
|
|
13698
|
+
get(name) {
|
|
13699
|
+
if (name in localStore.signals) {
|
|
13700
|
+
return localStore.get(name);
|
|
13701
|
+
}
|
|
13702
|
+
return globalState.get(name);
|
|
13703
|
+
},
|
|
13704
|
+
set(name, value) {
|
|
13705
|
+
globalState.set(name, value);
|
|
13706
|
+
},
|
|
13707
|
+
setPath(name, path, value) {
|
|
13708
|
+
globalState.setPath(name, path, value);
|
|
13709
|
+
},
|
|
13710
|
+
subscribe(name, fn) {
|
|
13711
|
+
if (name in localStore.signals) {
|
|
13712
|
+
return localStore.signals[name].subscribe(fn);
|
|
13713
|
+
}
|
|
13714
|
+
return globalState.subscribe(name, fn);
|
|
13715
|
+
},
|
|
13716
|
+
getPath(name, path) {
|
|
13717
|
+
return globalState.getPath(name, path);
|
|
13718
|
+
},
|
|
13719
|
+
subscribeToPath(name, path, fn) {
|
|
13720
|
+
return globalState.subscribeToPath(name, path, fn);
|
|
13721
|
+
}
|
|
13722
|
+
};
|
|
13723
|
+
}
|
|
13724
|
+
function renderLocalState(node, ctx) {
|
|
13725
|
+
const localStore = createLocalStateStore(node.state);
|
|
13726
|
+
const mergedLocals = createLocalsWithLocalState(ctx.locals, localStore);
|
|
13727
|
+
const mergedState = createStateWithLocalState(ctx.state, localStore);
|
|
13728
|
+
const mergedActions = { ...ctx.actions };
|
|
13729
|
+
for (const [name, action] of Object.entries(node.actions)) {
|
|
13730
|
+
mergedActions[name] = {
|
|
13731
|
+
...action,
|
|
13732
|
+
_isLocalAction: true,
|
|
13733
|
+
_localStore: localStore
|
|
13734
|
+
};
|
|
13735
|
+
}
|
|
13736
|
+
const childCtx = {
|
|
13737
|
+
...ctx,
|
|
13738
|
+
state: mergedState,
|
|
13739
|
+
locals: mergedLocals,
|
|
13740
|
+
actions: mergedActions,
|
|
13741
|
+
localState: {
|
|
13742
|
+
store: localStore,
|
|
13743
|
+
actions: node.actions
|
|
13744
|
+
}
|
|
13745
|
+
};
|
|
13746
|
+
return render(node.child, childCtx);
|
|
13747
|
+
}
|
|
13538
13748
|
|
|
13539
13749
|
// src/app.ts
|
|
13540
13750
|
function createApp(program, mount) {
|
|
@@ -13616,6 +13826,18 @@ function createReactiveLocals2(baseLocals, itemSignal, indexSignal, itemName, in
|
|
|
13616
13826
|
if (prop === itemName) return true;
|
|
13617
13827
|
if (indexName && prop === indexName) return true;
|
|
13618
13828
|
return prop in target;
|
|
13829
|
+
},
|
|
13830
|
+
ownKeys(target) {
|
|
13831
|
+
const keys = Reflect.ownKeys(target);
|
|
13832
|
+
if (!keys.includes(itemName)) keys.push(itemName);
|
|
13833
|
+
if (indexName && !keys.includes(indexName)) keys.push(indexName);
|
|
13834
|
+
return keys;
|
|
13835
|
+
},
|
|
13836
|
+
getOwnPropertyDescriptor(target, prop) {
|
|
13837
|
+
if (prop === itemName || indexName && prop === indexName) {
|
|
13838
|
+
return { enumerable: true, configurable: true };
|
|
13839
|
+
}
|
|
13840
|
+
return Reflect.getOwnPropertyDescriptor(target, prop);
|
|
13619
13841
|
}
|
|
13620
13842
|
});
|
|
13621
13843
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constela/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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.10.0",
|
|
22
|
+
"@constela/core": "0.10.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": "
|
|
32
|
+
"@constela/server": "6.0.0"
|
|
33
33
|
},
|
|
34
34
|
"engines": {
|
|
35
35
|
"node": ">=20.0.0"
|