@jxsuite/runtime 0.28.2 → 0.28.4
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/runtime.js +536 -329
- package/dist/runtime.js.map +8 -7
- package/package.json +3 -3
- package/src/expression.ts +198 -97
- package/src/runtime.ts +621 -462
- package/src/types.ts +13 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/runtime",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.4",
|
|
4
4
|
"description": "JSON-native reactive web component runtime",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"upgrade": "bunx npm-check-updates -u && bun install"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@jxsuite/schema": "^0.28.
|
|
32
|
-
"@vue/reactivity": "^3.5.
|
|
31
|
+
"@jxsuite/schema": "^0.28.4",
|
|
32
|
+
"@vue/reactivity": "^3.5.38"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@happy-dom/global-registrator": "^20.10.2"
|
package/src/expression.ts
CHANGED
|
@@ -7,11 +7,24 @@
|
|
|
7
7
|
* @module expression
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
import type { JxExpressionNode, JxExpressionOperand } from "@jxsuite/schema/types";
|
|
11
|
+
import type { JxScope } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
/** The runtime's expression node — the schema's expression model. */
|
|
14
|
+
export type ExpressionNode = JxExpressionNode;
|
|
15
|
+
export type ExpressionOperand = JxExpressionOperand;
|
|
16
|
+
|
|
17
|
+
/** The `$map` iteration context object stored in scope during mapped rendering. */
|
|
18
|
+
interface ScopeMapCtx {
|
|
19
|
+
item?: unknown;
|
|
20
|
+
index?: number;
|
|
21
|
+
[key: string]: unknown;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** View the scope's `$map` iteration context, if present. */
|
|
25
|
+
function scopeMap(state: JxScope): ScopeMapCtx | undefined {
|
|
26
|
+
const map = state.$map;
|
|
27
|
+
return map && typeof map === "object" ? (map as ScopeMapCtx) : undefined;
|
|
15
28
|
}
|
|
16
29
|
|
|
17
30
|
interface CompileOpts {
|
|
@@ -77,11 +90,13 @@ interface IterCtx {
|
|
|
77
90
|
/** Resolve an operand to its runtime value. */
|
|
78
91
|
function resolveOperand(
|
|
79
92
|
operand: unknown,
|
|
80
|
-
state:
|
|
93
|
+
state: JxScope,
|
|
81
94
|
event: Event | null,
|
|
82
95
|
iterCtx?: IterCtx,
|
|
83
96
|
): unknown {
|
|
84
|
-
if (operand === null || operand === undefined)
|
|
97
|
+
if (operand === null || operand === undefined) {
|
|
98
|
+
return operand;
|
|
99
|
+
}
|
|
85
100
|
|
|
86
101
|
// Nested expression node
|
|
87
102
|
if (typeof operand === "object" && !Array.isArray(operand) && "operator" in operand) {
|
|
@@ -103,12 +118,7 @@ function resolveOperand(
|
|
|
103
118
|
}
|
|
104
119
|
|
|
105
120
|
/** Resolve a $ref string within expression context. */
|
|
106
|
-
function resolveExprRef(
|
|
107
|
-
ref: string,
|
|
108
|
-
state: Record<string, any>,
|
|
109
|
-
event: Event | null,
|
|
110
|
-
iterCtx?: IterCtx,
|
|
111
|
-
) {
|
|
121
|
+
function resolveExprRef(ref: string, state: JxScope, event: Event | null, iterCtx?: IterCtx) {
|
|
112
122
|
if (ref === "$reduce/acc") {
|
|
113
123
|
return iterCtx?.acc;
|
|
114
124
|
}
|
|
@@ -121,68 +131,90 @@ function resolveExprRef(
|
|
|
121
131
|
}
|
|
122
132
|
if (ref.startsWith("$map/")) {
|
|
123
133
|
const parts = ref.split("/");
|
|
124
|
-
const key = parts
|
|
134
|
+
const [, key] = parts;
|
|
125
135
|
let base;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
const map = scopeMap(state);
|
|
137
|
+
if (key === "item") {
|
|
138
|
+
base = iterCtx?.item ?? map?.item ?? state["$map/item"];
|
|
139
|
+
} else if (key === "index") {
|
|
140
|
+
base = iterCtx?.index ?? map?.index ?? state["$map/index"];
|
|
141
|
+
} else {
|
|
142
|
+
base = map?.[key] ?? state[`$map/${key}`];
|
|
143
|
+
}
|
|
129
144
|
return parts.length > 2 ? getPath(base, parts.slice(2).join("/")) : base;
|
|
130
145
|
}
|
|
131
146
|
if (ref.startsWith("#/state/")) {
|
|
132
147
|
const sub = ref.slice("#/state/".length);
|
|
133
148
|
const slash = sub.indexOf("/");
|
|
134
|
-
if (slash
|
|
149
|
+
if (slash === -1) {
|
|
150
|
+
return state[sub];
|
|
151
|
+
}
|
|
135
152
|
return getPath(state[sub.slice(0, slash)], sub.slice(slash + 1));
|
|
136
153
|
}
|
|
137
|
-
if (ref.startsWith("parent#/"))
|
|
138
|
-
|
|
139
|
-
|
|
154
|
+
if (ref.startsWith("parent#/")) {
|
|
155
|
+
return state[ref.slice("parent#/".length)];
|
|
156
|
+
}
|
|
157
|
+
if (ref.startsWith("window#/")) {
|
|
158
|
+
return getPath(globalThis.window, ref.slice("window#/".length));
|
|
159
|
+
}
|
|
160
|
+
if (ref.startsWith("document#/")) {
|
|
140
161
|
return getPath(globalThis.document, ref.slice("document#/".length));
|
|
162
|
+
}
|
|
141
163
|
return state[ref] ?? null;
|
|
142
164
|
}
|
|
143
165
|
|
|
144
166
|
/** Resolve a $ref to a writable location — returns { obj, key } for assignment. */
|
|
145
167
|
function resolveWritableRef(
|
|
146
168
|
ref: string,
|
|
147
|
-
state:
|
|
169
|
+
state: JxScope,
|
|
148
170
|
event: Event | null,
|
|
149
171
|
iterCtx?: IterCtx,
|
|
150
|
-
) {
|
|
172
|
+
): { obj: JxScope; key: string } {
|
|
151
173
|
if (ref.startsWith("$map/")) {
|
|
152
174
|
const parts = ref.split("/");
|
|
153
|
-
const key = parts
|
|
175
|
+
const [, key] = parts;
|
|
154
176
|
let base;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
177
|
+
const map = scopeMap(state);
|
|
178
|
+
if (key === "item") {
|
|
179
|
+
base = iterCtx?.item ?? map?.item ?? state["$map/item"];
|
|
180
|
+
} else if (key === "index") {
|
|
181
|
+
base = iterCtx?.index ?? map?.index ?? state["$map/index"];
|
|
182
|
+
} else {
|
|
183
|
+
base = map?.[key] ?? state[`$map/${key}`];
|
|
184
|
+
}
|
|
158
185
|
if (parts.length > 2) {
|
|
159
186
|
const pathParts = parts.slice(2);
|
|
160
187
|
const lastKey = pathParts.pop();
|
|
161
188
|
const obj = pathParts.length > 0 ? getPath(base, pathParts.join("/")) : base;
|
|
162
|
-
return {
|
|
189
|
+
return { key: lastKey as string, obj: obj as JxScope };
|
|
163
190
|
}
|
|
164
191
|
return {
|
|
165
|
-
obj: state.$map ?? state,
|
|
166
192
|
key: key === "item" ? "$map/item" : "$map/index",
|
|
193
|
+
obj: scopeMap(state) ?? state,
|
|
167
194
|
};
|
|
168
195
|
}
|
|
169
196
|
if (ref.startsWith("#/state/")) {
|
|
170
197
|
const sub = ref.slice("#/state/".length);
|
|
171
198
|
const slash = sub.indexOf("/");
|
|
172
|
-
if (slash
|
|
199
|
+
if (slash === -1) {
|
|
200
|
+
return { key: sub, obj: state };
|
|
201
|
+
}
|
|
173
202
|
const parts = sub.split("/");
|
|
174
203
|
const lastKey = parts.pop();
|
|
175
204
|
let obj = state;
|
|
176
|
-
|
|
177
|
-
|
|
205
|
+
// Pointer paths address objects by construction (validated by the schema).
|
|
206
|
+
for (const p of parts) {
|
|
207
|
+
obj = obj[p] as JxScope;
|
|
208
|
+
}
|
|
209
|
+
return { key: lastKey as string, obj };
|
|
178
210
|
}
|
|
179
|
-
return {
|
|
211
|
+
return { key: ref, obj: state };
|
|
180
212
|
}
|
|
181
213
|
|
|
182
214
|
/** Evaluate an expression node at runtime. */
|
|
183
215
|
export function evaluateExpression(
|
|
184
216
|
node: ExpressionNode,
|
|
185
|
-
state:
|
|
217
|
+
state: JxScope,
|
|
186
218
|
event: Event | null,
|
|
187
219
|
iterCtx?: IterCtx,
|
|
188
220
|
): unknown {
|
|
@@ -195,8 +227,12 @@ export function evaluateExpression(
|
|
|
195
227
|
// ─── Unary ───
|
|
196
228
|
if (UNARY_OPS.has(operator) && !("value" in node)) {
|
|
197
229
|
const operand: unknown = resolveOperand(target, state, event, iterCtx);
|
|
198
|
-
if (operator === "!")
|
|
199
|
-
|
|
230
|
+
if (operator === "!") {
|
|
231
|
+
return !operand;
|
|
232
|
+
}
|
|
233
|
+
if (operator === "-") {
|
|
234
|
+
return -(operand as number);
|
|
235
|
+
}
|
|
200
236
|
}
|
|
201
237
|
|
|
202
238
|
// ─── Binary (pure) ───
|
|
@@ -204,32 +240,48 @@ export function evaluateExpression(
|
|
|
204
240
|
const left = resolveOperand(target, state, event, iterCtx) as number & string;
|
|
205
241
|
const right = resolveOperand(value, state, event, iterCtx) as number & string;
|
|
206
242
|
switch (operator) {
|
|
207
|
-
case "+":
|
|
243
|
+
case "+": {
|
|
208
244
|
return left + right;
|
|
209
|
-
|
|
245
|
+
}
|
|
246
|
+
case "-": {
|
|
210
247
|
return left - right;
|
|
211
|
-
|
|
248
|
+
}
|
|
249
|
+
case "*": {
|
|
212
250
|
return left * right;
|
|
213
|
-
|
|
251
|
+
}
|
|
252
|
+
case "/": {
|
|
214
253
|
return left / right;
|
|
215
|
-
|
|
254
|
+
}
|
|
255
|
+
case "%": {
|
|
216
256
|
return left % right;
|
|
217
|
-
|
|
257
|
+
}
|
|
258
|
+
case "===": {
|
|
218
259
|
return left === right;
|
|
219
|
-
|
|
260
|
+
}
|
|
261
|
+
case "!==": {
|
|
220
262
|
return left !== right;
|
|
221
|
-
|
|
263
|
+
}
|
|
264
|
+
case "<": {
|
|
222
265
|
return left < right;
|
|
223
|
-
|
|
266
|
+
}
|
|
267
|
+
case "<=": {
|
|
224
268
|
return left <= right;
|
|
225
|
-
|
|
269
|
+
}
|
|
270
|
+
case ">": {
|
|
226
271
|
return left > right;
|
|
227
|
-
|
|
272
|
+
}
|
|
273
|
+
case ">=": {
|
|
228
274
|
return left >= right;
|
|
229
|
-
|
|
275
|
+
}
|
|
276
|
+
case "&&": {
|
|
230
277
|
return left && right;
|
|
231
|
-
|
|
278
|
+
}
|
|
279
|
+
case "||": {
|
|
232
280
|
return left || right;
|
|
281
|
+
}
|
|
282
|
+
default: {
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
233
285
|
}
|
|
234
286
|
}
|
|
235
287
|
|
|
@@ -243,21 +295,33 @@ export function evaluateExpression(
|
|
|
243
295
|
iterCtx,
|
|
244
296
|
);
|
|
245
297
|
switch (operator) {
|
|
246
|
-
case "=":
|
|
298
|
+
case "=": {
|
|
247
299
|
obj[key] = rhs;
|
|
248
300
|
break;
|
|
249
|
-
|
|
250
|
-
|
|
301
|
+
}
|
|
302
|
+
case "+=": {
|
|
303
|
+
const current = obj[key] as number;
|
|
304
|
+
obj[key] = current + rhs;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
case "-=": {
|
|
308
|
+
const current = obj[key] as number;
|
|
309
|
+
obj[key] = current - rhs;
|
|
251
310
|
break;
|
|
252
|
-
|
|
253
|
-
|
|
311
|
+
}
|
|
312
|
+
case "*=": {
|
|
313
|
+
const current = obj[key] as number;
|
|
314
|
+
obj[key] = current * rhs;
|
|
254
315
|
break;
|
|
255
|
-
|
|
256
|
-
|
|
316
|
+
}
|
|
317
|
+
case "/=": {
|
|
318
|
+
const current = obj[key] as number;
|
|
319
|
+
obj[key] = current / rhs;
|
|
257
320
|
break;
|
|
258
|
-
|
|
259
|
-
|
|
321
|
+
}
|
|
322
|
+
default: {
|
|
260
323
|
break;
|
|
324
|
+
}
|
|
261
325
|
}
|
|
262
326
|
return;
|
|
263
327
|
}
|
|
@@ -266,18 +330,25 @@ export function evaluateExpression(
|
|
|
266
330
|
if (ARRAY_METHOD_OPS.has(operator)) {
|
|
267
331
|
const arr: unknown[] = resolveOperand(target, state, event, iterCtx) as unknown[];
|
|
268
332
|
switch (operator) {
|
|
269
|
-
case "push":
|
|
333
|
+
case "push": {
|
|
270
334
|
return arr.push(resolveOperand(value, state, event, iterCtx));
|
|
271
|
-
|
|
335
|
+
}
|
|
336
|
+
case "unshift": {
|
|
272
337
|
return arr.unshift(resolveOperand(value, state, event, iterCtx));
|
|
273
|
-
|
|
338
|
+
}
|
|
339
|
+
case "pop": {
|
|
274
340
|
return arr.pop();
|
|
275
|
-
|
|
341
|
+
}
|
|
342
|
+
case "shift": {
|
|
276
343
|
return arr.shift();
|
|
344
|
+
}
|
|
277
345
|
case "splice": {
|
|
278
346
|
const args: unknown[] = resolveOperand(value, state, event, iterCtx) as unknown[];
|
|
279
347
|
return (arr.splice as (...a: unknown[]) => unknown[])(...args);
|
|
280
348
|
}
|
|
349
|
+
default: {
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
281
352
|
}
|
|
282
353
|
}
|
|
283
354
|
|
|
@@ -285,32 +356,33 @@ export function evaluateExpression(
|
|
|
285
356
|
if (AGGREGATE_OPS.has(operator)) {
|
|
286
357
|
const arr: unknown[] = resolveOperand(target, state, event, iterCtx) as unknown[];
|
|
287
358
|
if (operator === "reduce") {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
359
|
+
let acc: unknown = resolveOperand(initial, state, event, iterCtx);
|
|
360
|
+
for (const [index, item] of arr.entries()) {
|
|
361
|
+
acc = evaluateExpression(value as ExpressionNode, state, event, {
|
|
291
362
|
acc,
|
|
292
|
-
item,
|
|
293
363
|
index,
|
|
364
|
+
item,
|
|
294
365
|
});
|
|
295
|
-
}
|
|
366
|
+
}
|
|
367
|
+
return acc;
|
|
296
368
|
}
|
|
297
369
|
if (operator === "map") {
|
|
298
|
-
return arr.map((item: unknown, index: number) =>
|
|
299
|
-
|
|
370
|
+
return arr.map((item: unknown, index: number) =>
|
|
371
|
+
evaluateExpression(value as ExpressionNode, state, event, {
|
|
300
372
|
...iterCtx,
|
|
301
|
-
item,
|
|
302
373
|
index,
|
|
303
|
-
|
|
304
|
-
|
|
374
|
+
item,
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
305
377
|
}
|
|
306
378
|
if (operator === "filter") {
|
|
307
|
-
return arr.filter((item: unknown, index: number) =>
|
|
308
|
-
|
|
379
|
+
return arr.filter((item: unknown, index: number) =>
|
|
380
|
+
evaluateExpression(value as ExpressionNode, state, event, {
|
|
309
381
|
...iterCtx,
|
|
310
|
-
item,
|
|
311
382
|
index,
|
|
312
|
-
|
|
313
|
-
|
|
383
|
+
item,
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
314
386
|
}
|
|
315
387
|
}
|
|
316
388
|
|
|
@@ -321,8 +393,12 @@ export function evaluateExpression(
|
|
|
321
393
|
|
|
322
394
|
/** Compile an operand to a JS source string. */
|
|
323
395
|
function compileOperand(operand: unknown, opts: CompileOpts): string {
|
|
324
|
-
if (operand === null)
|
|
325
|
-
|
|
396
|
+
if (operand === null) {
|
|
397
|
+
return "null";
|
|
398
|
+
}
|
|
399
|
+
if (operand === undefined) {
|
|
400
|
+
return "undefined";
|
|
401
|
+
}
|
|
326
402
|
|
|
327
403
|
// Nested expression node
|
|
328
404
|
if (typeof operand === "object" && !Array.isArray(operand) && "operator" in operand) {
|
|
@@ -354,34 +430,44 @@ function compileRef(ref: string, opts: CompileOpts) {
|
|
|
354
430
|
const s = opts.statePrefix ?? "state";
|
|
355
431
|
const e = opts.eventParam ?? "event";
|
|
356
432
|
|
|
357
|
-
if (ref === "$reduce/acc")
|
|
358
|
-
|
|
433
|
+
if (ref === "$reduce/acc") {
|
|
434
|
+
return "_acc";
|
|
435
|
+
}
|
|
436
|
+
if (ref.startsWith("$reduce/")) {
|
|
437
|
+
return "_acc";
|
|
438
|
+
}
|
|
359
439
|
|
|
360
440
|
if (ref.startsWith("event#/")) {
|
|
361
441
|
const path = ref.slice("event#/".length);
|
|
362
|
-
return `${e}.${path.
|
|
442
|
+
return `${e}.${path.replaceAll("/", ".")}`;
|
|
363
443
|
}
|
|
364
444
|
|
|
365
445
|
if (ref.startsWith("$map/")) {
|
|
366
446
|
const parts = ref.split("/");
|
|
367
|
-
const key = parts
|
|
447
|
+
const [, key] = parts;
|
|
368
448
|
if (key === "item") {
|
|
369
449
|
return parts.length > 2 ? `_item.${parts.slice(2).join(".")}` : "_item";
|
|
370
450
|
}
|
|
371
|
-
if (key === "index")
|
|
451
|
+
if (key === "index") {
|
|
452
|
+
return "_index";
|
|
453
|
+
}
|
|
372
454
|
return `_${key}`;
|
|
373
455
|
}
|
|
374
456
|
|
|
375
457
|
if (ref.startsWith("#/state/")) {
|
|
376
458
|
const path = ref.slice("#/state/".length);
|
|
377
|
-
return `${s}.${path.
|
|
459
|
+
return `${s}.${path.replaceAll("/", ".")}`;
|
|
378
460
|
}
|
|
379
461
|
|
|
380
|
-
if (ref.startsWith("parent#/"))
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (ref.startsWith("
|
|
384
|
-
return `
|
|
462
|
+
if (ref.startsWith("parent#/")) {
|
|
463
|
+
return `${s}.${ref.slice("parent#/".length)}`;
|
|
464
|
+
}
|
|
465
|
+
if (ref.startsWith("window#/")) {
|
|
466
|
+
return `window.${ref.slice("window#/".length).replaceAll("/", ".")}`;
|
|
467
|
+
}
|
|
468
|
+
if (ref.startsWith("document#/")) {
|
|
469
|
+
return `document.${ref.slice("document#/".length).replaceAll("/", ".")}`;
|
|
470
|
+
}
|
|
385
471
|
|
|
386
472
|
return `${s}.${ref}`;
|
|
387
473
|
}
|
|
@@ -408,8 +494,12 @@ export function compileExpression(node: ExpressionNode, opts: CompileOpts = {}):
|
|
|
408
494
|
// ─── Unary ───
|
|
409
495
|
if (UNARY_OPS.has(operator) && !("value" in node)) {
|
|
410
496
|
const operand: string = compileOperand(target, opts);
|
|
411
|
-
if (operator === "!")
|
|
412
|
-
|
|
497
|
+
if (operator === "!") {
|
|
498
|
+
return `!(${operand})`;
|
|
499
|
+
}
|
|
500
|
+
if (operator === "-") {
|
|
501
|
+
return `-(${operand})`;
|
|
502
|
+
}
|
|
413
503
|
}
|
|
414
504
|
|
|
415
505
|
// ─── Binary (pure) ───
|
|
@@ -430,20 +520,27 @@ export function compileExpression(node: ExpressionNode, opts: CompileOpts = {}):
|
|
|
430
520
|
if (ARRAY_METHOD_OPS.has(operator)) {
|
|
431
521
|
const arr: string = compileTarget(target, opts);
|
|
432
522
|
switch (operator) {
|
|
433
|
-
case "push":
|
|
523
|
+
case "push": {
|
|
434
524
|
return `${arr}.push(${compileOperand(value, opts)})`;
|
|
435
|
-
|
|
525
|
+
}
|
|
526
|
+
case "unshift": {
|
|
436
527
|
return `${arr}.unshift(${compileOperand(value, opts)})`;
|
|
437
|
-
|
|
528
|
+
}
|
|
529
|
+
case "pop": {
|
|
438
530
|
return `${arr}.pop()`;
|
|
439
|
-
|
|
531
|
+
}
|
|
532
|
+
case "shift": {
|
|
440
533
|
return `${arr}.shift()`;
|
|
534
|
+
}
|
|
441
535
|
case "splice": {
|
|
442
536
|
const args: string = Array.isArray(value)
|
|
443
537
|
? value.map((o: unknown) => compileOperand(o, opts)).join(", ")
|
|
444
538
|
: compileOperand(value, opts);
|
|
445
539
|
return `${arr}.splice(${args})`;
|
|
446
540
|
}
|
|
541
|
+
default: {
|
|
542
|
+
break;
|
|
543
|
+
}
|
|
447
544
|
}
|
|
448
545
|
}
|
|
449
546
|
|
|
@@ -470,5 +567,9 @@ export function compileExpression(node: ExpressionNode, opts: CompileOpts = {}):
|
|
|
470
567
|
|
|
471
568
|
/** Resolve a dotted/slashed path on an object. */
|
|
472
569
|
function getPath(obj: unknown, path: string): unknown {
|
|
473
|
-
|
|
570
|
+
let current: unknown = obj;
|
|
571
|
+
for (const k of path.split(/[./]/)) {
|
|
572
|
+
current = (current as Record<string, unknown>)?.[k];
|
|
573
|
+
}
|
|
574
|
+
return current;
|
|
474
575
|
}
|