@jxsuite/runtime 0.28.2 → 0.28.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/runtime",
3
- "version": "0.28.2",
3
+ "version": "0.28.3",
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.2",
32
- "@vue/reactivity": "^3.5.35"
31
+ "@jxsuite/schema": "^0.28.3",
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
- export interface ExpressionNode {
11
- operator: string;
12
- target: unknown;
13
- value?: unknown;
14
- initial?: unknown;
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: Record<string, any>,
93
+ state: JxScope,
81
94
  event: Event | null,
82
95
  iterCtx?: IterCtx,
83
96
  ): unknown {
84
- if (operand === null || operand === undefined) return operand;
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[1];
134
+ const [, key] = parts;
125
135
  let base;
126
- if (key === "item") base = iterCtx?.item ?? state.$map?.item ?? state["$map/item"];
127
- else if (key === "index") base = iterCtx?.index ?? state.$map?.index ?? state["$map/index"];
128
- else base = state.$map?.[key] ?? state["$map/" + key];
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 < 0) return state[sub];
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#/")) return state[ref.slice("parent#/".length)];
138
- if (ref.startsWith("window#/")) return getPath(globalThis.window, ref.slice("window#/".length));
139
- if (ref.startsWith("document#/"))
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: Record<string, any>,
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[1];
175
+ const [, key] = parts;
154
176
  let base;
155
- if (key === "item") base = iterCtx?.item ?? state.$map?.item ?? state["$map/item"];
156
- else if (key === "index") base = iterCtx?.index ?? state.$map?.index ?? state["$map/index"];
157
- else base = state.$map?.[key] ?? state["$map/" + key];
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 { obj, key: lastKey as string };
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 < 0) return { obj: state, key: sub };
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
- for (const p of parts) obj = obj[p];
177
- return { obj, key: lastKey as string };
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 { obj: state, key: ref };
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: Record<string, any>,
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 === "!") return !operand;
199
- if (operator === "-") return -(operand as number);
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
- case "-":
245
+ }
246
+ case "-": {
210
247
  return left - right;
211
- case "*":
248
+ }
249
+ case "*": {
212
250
  return left * right;
213
- case "/":
251
+ }
252
+ case "/": {
214
253
  return left / right;
215
- case "%":
254
+ }
255
+ case "%": {
216
256
  return left % right;
217
- case "===":
257
+ }
258
+ case "===": {
218
259
  return left === right;
219
- case "!==":
260
+ }
261
+ case "!==": {
220
262
  return left !== right;
221
- case "<":
263
+ }
264
+ case "<": {
222
265
  return left < right;
223
- case "<=":
266
+ }
267
+ case "<=": {
224
268
  return left <= right;
225
- case ">":
269
+ }
270
+ case ">": {
226
271
  return left > right;
227
- case ">=":
272
+ }
273
+ case ">=": {
228
274
  return left >= right;
229
- case "&&":
275
+ }
276
+ case "&&": {
230
277
  return left && right;
231
- case "||":
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
- case "+=":
250
- obj[key] += rhs;
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
- case "-=":
253
- obj[key] -= rhs;
311
+ }
312
+ case "*=": {
313
+ const current = obj[key] as number;
314
+ obj[key] = current * rhs;
254
315
  break;
255
- case "*=":
256
- obj[key] *= rhs;
316
+ }
317
+ case "/=": {
318
+ const current = obj[key] as number;
319
+ obj[key] = current / rhs;
257
320
  break;
258
- case "/=":
259
- obj[key] /= rhs;
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
- case "unshift":
335
+ }
336
+ case "unshift": {
272
337
  return arr.unshift(resolveOperand(value, state, event, iterCtx));
273
- case "pop":
338
+ }
339
+ case "pop": {
274
340
  return arr.pop();
275
- case "shift":
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
- const seed: unknown = resolveOperand(initial, state, event, iterCtx);
289
- return arr.reduce((acc: unknown, item: unknown, index: number) => {
290
- return evaluateExpression(value as ExpressionNode, state, event, {
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
- }, seed);
366
+ }
367
+ return acc;
296
368
  }
297
369
  if (operator === "map") {
298
- return arr.map((item: unknown, index: number) => {
299
- return evaluateExpression(value as ExpressionNode, state, event, {
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
- return evaluateExpression(value as ExpressionNode, state, event, {
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) return "null";
325
- if (operand === undefined) return "undefined";
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") return "_acc";
358
- if (ref.startsWith("$reduce/")) return "_acc";
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.replace(/\//g, ".")}`;
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[1];
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") return "_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.replace(/\//g, ".")}`;
459
+ return `${s}.${path.replaceAll("/", ".")}`;
378
460
  }
379
461
 
380
- if (ref.startsWith("parent#/")) return `${s}.${ref.slice("parent#/".length)}`;
381
- if (ref.startsWith("window#/"))
382
- return `window.${ref.slice("window#/".length).replace(/\//g, ".")}`;
383
- if (ref.startsWith("document#/"))
384
- return `document.${ref.slice("document#/".length).replace(/\//g, ".")}`;
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 === "!") return `!(${operand})`;
412
- if (operator === "-") return `-(${operand})`;
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
- case "unshift":
525
+ }
526
+ case "unshift": {
436
527
  return `${arr}.unshift(${compileOperand(value, opts)})`;
437
- case "pop":
528
+ }
529
+ case "pop": {
438
530
  return `${arr}.pop()`;
439
- case "shift":
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
- return path.split(/[./]/).reduce((o, k) => (o as Record<string, unknown>)?.[k], obj);
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
  }